@utilia-os/sdk-js 1.7.0 → 2.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/README.md +168 -0
- package/dist/index.d.mts +1412 -16
- package/dist/index.d.ts +1412 -16
- package/dist/index.js +1122 -37
- package/dist/index.mjs +1121 -37
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
BUDGET_WEBHOOK_EVENTS: () => BUDGET_WEBHOOK_EVENTS,
|
|
33
34
|
ErrorCode: () => ErrorCode,
|
|
34
35
|
FileTokenStorage: () => FileTokenStorage,
|
|
35
36
|
MemoryTokenStorage: () => MemoryTokenStorage,
|
|
@@ -216,6 +217,8 @@ var OAuthService = class {
|
|
|
216
217
|
constructor(baseURL, config) {
|
|
217
218
|
/** Estado PKCE en memoria como fallback cuando el storage no soporta pendingState */
|
|
218
219
|
this._pendingState = null;
|
|
220
|
+
/** Promesa de refresco en curso para deduplicar llamadas concurrentes. */
|
|
221
|
+
this._refreshInFlight = null;
|
|
219
222
|
this.baseURL = baseURL.replace(/\/$/, "");
|
|
220
223
|
this.config = config;
|
|
221
224
|
this.storage = config.tokenStorage ?? new MemoryTokenStorage();
|
|
@@ -231,7 +234,7 @@ var OAuthService = class {
|
|
|
231
234
|
*
|
|
232
235
|
* Este es el método recomendado para la mayoría de los casos.
|
|
233
236
|
*
|
|
234
|
-
* @param options - Opciones adicionales (state personalizado, scopes)
|
|
237
|
+
* @param options - Opciones adicionales (state personalizado, scopes, parámetros OIDC)
|
|
235
238
|
* @returns URL de autorización lista para redirigir al usuario
|
|
236
239
|
*/
|
|
237
240
|
async getAuthorizationUrl(options) {
|
|
@@ -252,8 +255,8 @@ var OAuthService = class {
|
|
|
252
255
|
* Método de control manual: devuelve codeVerifier y state que el desarrollador
|
|
253
256
|
* debe gestionar. Para un flujo automático, usar getAuthorizationUrl() en su lugar.
|
|
254
257
|
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
258
|
+
* Soporta los parámetros OIDC Core 1.0 §3.1.2.1: prompt, loginHint, nonce,
|
|
259
|
+
* maxAge y el Resource Indicator (RFC 8707) mediante el campo resource.
|
|
257
260
|
*/
|
|
258
261
|
async getLoginUrl(options) {
|
|
259
262
|
const codeVerifier = generateCodeVerifier();
|
|
@@ -269,6 +272,24 @@ var OAuthService = class {
|
|
|
269
272
|
code_challenge: codeChallenge,
|
|
270
273
|
code_challenge_method: "S256"
|
|
271
274
|
});
|
|
275
|
+
if (options?.theme) {
|
|
276
|
+
params.set("theme", options.theme);
|
|
277
|
+
}
|
|
278
|
+
if (options?.prompt) {
|
|
279
|
+
params.set("prompt", options.prompt);
|
|
280
|
+
}
|
|
281
|
+
if (options?.loginHint) {
|
|
282
|
+
params.set("login_hint", options.loginHint);
|
|
283
|
+
}
|
|
284
|
+
if (options?.nonce) {
|
|
285
|
+
params.set("nonce", options.nonce);
|
|
286
|
+
}
|
|
287
|
+
if (options?.resource) {
|
|
288
|
+
params.set("resource", options.resource);
|
|
289
|
+
}
|
|
290
|
+
if (typeof options?.maxAge === "number" && Number.isFinite(options.maxAge) && options.maxAge >= 0) {
|
|
291
|
+
params.set("max_age", String(Math.floor(options.maxAge)));
|
|
292
|
+
}
|
|
272
293
|
return {
|
|
273
294
|
url: `${this.baseURL}/oauth/authorize?${params.toString()}`,
|
|
274
295
|
codeVerifier,
|
|
@@ -288,7 +309,13 @@ var OAuthService = class {
|
|
|
288
309
|
}
|
|
289
310
|
const { url, codeVerifier, state } = await this.getLoginUrl({
|
|
290
311
|
state: options?.state,
|
|
291
|
-
scopes: options?.scopes
|
|
312
|
+
scopes: options?.scopes,
|
|
313
|
+
theme: options?.theme,
|
|
314
|
+
prompt: options?.prompt,
|
|
315
|
+
loginHint: options?.loginHint,
|
|
316
|
+
nonce: options?.nonce,
|
|
317
|
+
resource: options?.resource,
|
|
318
|
+
maxAge: options?.maxAge
|
|
292
319
|
});
|
|
293
320
|
const width = options?.width ?? 500;
|
|
294
321
|
const height = options?.height ?? 700;
|
|
@@ -406,37 +433,53 @@ var OAuthService = class {
|
|
|
406
433
|
return tokens;
|
|
407
434
|
}
|
|
408
435
|
/**
|
|
409
|
-
* Refresca el token de acceso usando el refresh_token
|
|
436
|
+
* Refresca el token de acceso usando el refresh_token.
|
|
437
|
+
*
|
|
438
|
+
* Deduplica peticiones concurrentes mediante una promesa cacheada: si dos
|
|
439
|
+
* llamadas entran a la vez, comparten la misma petición HTTP. Esto es
|
|
440
|
+
* imprescindible porque el backend implementa rotación de refresh tokens con
|
|
441
|
+
* detección de reuso; dos refresh concurrentes con el mismo token producirían
|
|
442
|
+
* una invalidación inmediata de toda la cadena.
|
|
410
443
|
*
|
|
411
444
|
* @returns Nuevos tokens OAuth
|
|
412
445
|
* @throws Error si no hay refresh_token disponible
|
|
413
446
|
*/
|
|
414
447
|
async refreshToken() {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
throw new Error("No hay refresh token disponible. El usuario debe volver a autenticarse.");
|
|
418
|
-
}
|
|
419
|
-
const body = {
|
|
420
|
-
grant_type: "refresh_token",
|
|
421
|
-
refresh_token: current.refreshToken,
|
|
422
|
-
client_id: this.config.clientId
|
|
423
|
-
};
|
|
424
|
-
if (this.config.clientSecret) {
|
|
425
|
-
body.client_secret = this.config.clientSecret;
|
|
448
|
+
if (this._refreshInFlight) {
|
|
449
|
+
return this._refreshInFlight;
|
|
426
450
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
451
|
+
this._refreshInFlight = (async () => {
|
|
452
|
+
try {
|
|
453
|
+
const current = await this.storage.getTokens();
|
|
454
|
+
if (!current?.refreshToken) {
|
|
455
|
+
throw new Error("No hay refresh token disponible. El usuario debe volver a autenticarse.");
|
|
456
|
+
}
|
|
457
|
+
const body = {
|
|
458
|
+
grant_type: "refresh_token",
|
|
459
|
+
refresh_token: current.refreshToken,
|
|
460
|
+
client_id: this.config.clientId
|
|
461
|
+
};
|
|
462
|
+
if (this.config.clientSecret) {
|
|
463
|
+
body.client_secret = this.config.clientSecret;
|
|
464
|
+
}
|
|
465
|
+
const response = await this.http.post("/oauth/token", body);
|
|
466
|
+
const data = response.data;
|
|
467
|
+
const tokens = {
|
|
468
|
+
accessToken: data.access_token,
|
|
469
|
+
refreshToken: data.refresh_token ?? current.refreshToken,
|
|
470
|
+
expiresIn: data.expires_in,
|
|
471
|
+
expiresAt: Date.now() + data.expires_in * 1e3,
|
|
472
|
+
tokenType: data.token_type || "Bearer",
|
|
473
|
+
scope: data.scope,
|
|
474
|
+
idToken: data.id_token
|
|
475
|
+
};
|
|
476
|
+
await this.storage.setTokens(tokens);
|
|
477
|
+
return tokens;
|
|
478
|
+
} finally {
|
|
479
|
+
this._refreshInFlight = null;
|
|
480
|
+
}
|
|
481
|
+
})();
|
|
482
|
+
return this._refreshInFlight;
|
|
440
483
|
}
|
|
441
484
|
/**
|
|
442
485
|
* Obtiene información del usuario autenticado
|
|
@@ -451,9 +494,9 @@ var OAuthService = class {
|
|
|
451
494
|
return response.data;
|
|
452
495
|
}
|
|
453
496
|
/**
|
|
454
|
-
* Revoca el token actual
|
|
497
|
+
* Revoca el token actual almacenado y limpia el storage.
|
|
455
498
|
*
|
|
456
|
-
* @param tokenType - Tipo de token a revocar (
|
|
499
|
+
* @param tokenType - Tipo de token a revocar (`access_token` o `refresh_token`).
|
|
457
500
|
* Por defecto revoca el access_token.
|
|
458
501
|
*/
|
|
459
502
|
async revokeToken(tokenType) {
|
|
@@ -461,21 +504,76 @@ var OAuthService = class {
|
|
|
461
504
|
if (!current) return;
|
|
462
505
|
const token = tokenType === "refresh_token" ? current.refreshToken : current.accessToken;
|
|
463
506
|
if (!token) return;
|
|
507
|
+
try {
|
|
508
|
+
await this.revoke(token, tokenType);
|
|
509
|
+
} catch {
|
|
510
|
+
}
|
|
511
|
+
await this.storage.clearTokens();
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Revoca un token arbitrario contra el endpoint `/oauth/revoke` (RFC 7009).
|
|
515
|
+
* A diferencia de `revokeToken()`, no toca el almacenamiento local.
|
|
516
|
+
*
|
|
517
|
+
* @param token - Token a revocar (access token o refresh token).
|
|
518
|
+
* @param tokenTypeHint - Pista opcional sobre el tipo de token.
|
|
519
|
+
*/
|
|
520
|
+
async revoke(token, tokenTypeHint) {
|
|
464
521
|
const body = {
|
|
465
522
|
token,
|
|
466
523
|
client_id: this.config.clientId
|
|
467
524
|
};
|
|
468
|
-
if (
|
|
469
|
-
body.token_type_hint =
|
|
525
|
+
if (tokenTypeHint) {
|
|
526
|
+
body.token_type_hint = tokenTypeHint;
|
|
470
527
|
}
|
|
471
528
|
if (this.config.clientSecret) {
|
|
472
529
|
body.client_secret = this.config.clientSecret;
|
|
473
530
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
531
|
+
await this.http.post("/oauth/revoke", body);
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Introspecciona un token contra `/oauth/introspect` (RFC 7662).
|
|
535
|
+
* Requiere que el cliente esté autenticado (client_secret o public con PKCE).
|
|
536
|
+
*
|
|
537
|
+
* @param token - Token a introspeccionar.
|
|
538
|
+
* @param tokenTypeHint - Pista opcional sobre el tipo de token.
|
|
539
|
+
* @returns Estado y metadatos del token.
|
|
540
|
+
*/
|
|
541
|
+
async introspect(token, tokenTypeHint) {
|
|
542
|
+
const body = {
|
|
543
|
+
token,
|
|
544
|
+
client_id: this.config.clientId
|
|
545
|
+
};
|
|
546
|
+
if (tokenTypeHint) {
|
|
547
|
+
body.token_type_hint = tokenTypeHint;
|
|
477
548
|
}
|
|
478
|
-
|
|
549
|
+
if (this.config.clientSecret) {
|
|
550
|
+
body.client_secret = this.config.clientSecret;
|
|
551
|
+
}
|
|
552
|
+
const response = await this.http.post("/oauth/introspect", body);
|
|
553
|
+
return response.data;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Lista las aplicaciones OAuth autorizadas por el usuario actual.
|
|
557
|
+
* Requiere un access token válido.
|
|
558
|
+
*/
|
|
559
|
+
async getAuthorizedApps() {
|
|
560
|
+
const accessToken = await this.getAccessToken();
|
|
561
|
+
const response = await this.http.get("/oauth/apps", {
|
|
562
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
563
|
+
});
|
|
564
|
+
return response.data;
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Revoca el acceso de una aplicación OAuth concreta del usuario actual.
|
|
568
|
+
* Todos los tokens emitidos para esa aplicación quedan invalidados.
|
|
569
|
+
*/
|
|
570
|
+
async revokeApp(clientId) {
|
|
571
|
+
const accessToken = await this.getAccessToken();
|
|
572
|
+
await this.http.post(
|
|
573
|
+
`/oauth/apps/${encodeURIComponent(clientId)}/revoke`,
|
|
574
|
+
{},
|
|
575
|
+
{ headers: { Authorization: `Bearer ${accessToken}` } }
|
|
576
|
+
);
|
|
479
577
|
}
|
|
480
578
|
/**
|
|
481
579
|
* Obtiene un access token válido, refrescándolo automáticamente si ha expirado
|
|
@@ -1712,6 +1810,987 @@ var AiService = class {
|
|
|
1712
1810
|
}
|
|
1713
1811
|
};
|
|
1714
1812
|
|
|
1813
|
+
// src/services/budgets.service.ts
|
|
1814
|
+
var BudgetsService = class {
|
|
1815
|
+
constructor(client) {
|
|
1816
|
+
this.basePath = "/crm/budgets";
|
|
1817
|
+
this.client = client;
|
|
1818
|
+
}
|
|
1819
|
+
// ==========================================
|
|
1820
|
+
// Listado / detalle / CRUD
|
|
1821
|
+
// ==========================================
|
|
1822
|
+
/**
|
|
1823
|
+
* Listar presupuestos aplicando filtros.
|
|
1824
|
+
*
|
|
1825
|
+
* @example
|
|
1826
|
+
* ```typescript
|
|
1827
|
+
* const page = await sdk.budgets.list({ status: 'SENT', page: 1, limit: 20 });
|
|
1828
|
+
* console.log(page.data, page.pagination.total);
|
|
1829
|
+
* ```
|
|
1830
|
+
*/
|
|
1831
|
+
async list(filters) {
|
|
1832
|
+
const params = this.serializeFilters(filters);
|
|
1833
|
+
const raw = await this.client.get(this.basePath, {
|
|
1834
|
+
params
|
|
1835
|
+
});
|
|
1836
|
+
return {
|
|
1837
|
+
data: raw.data ?? raw.budgets ?? [],
|
|
1838
|
+
pagination: raw.pagination
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
/** Obtener el detalle completo de un presupuesto por UUID. */
|
|
1842
|
+
async get(id) {
|
|
1843
|
+
validateRequired(id, "id");
|
|
1844
|
+
return this.client.get(`${this.basePath}/${id}`);
|
|
1845
|
+
}
|
|
1846
|
+
/**
|
|
1847
|
+
* Crear un nuevo presupuesto.
|
|
1848
|
+
* Puede incluir items y secciones en la misma peticion.
|
|
1849
|
+
*/
|
|
1850
|
+
async create(data) {
|
|
1851
|
+
validateRequired(data.title, "title");
|
|
1852
|
+
validateRequired(data.clientId, "clientId");
|
|
1853
|
+
validateRequired(data.validUntil, "validUntil");
|
|
1854
|
+
validateStringLength(data.title, "title", 1, 200);
|
|
1855
|
+
return this.client.post(this.basePath, data);
|
|
1856
|
+
}
|
|
1857
|
+
/** Actualizar un presupuesto (solo en estado DRAFT salvo excepciones). */
|
|
1858
|
+
async update(id, data) {
|
|
1859
|
+
validateRequired(id, "id");
|
|
1860
|
+
return this.client.patch(`${this.basePath}/${id}`, data);
|
|
1861
|
+
}
|
|
1862
|
+
/** Soft delete (puede recuperarse). */
|
|
1863
|
+
async delete(id) {
|
|
1864
|
+
validateRequired(id, "id");
|
|
1865
|
+
return this.client.delete(`${this.basePath}/${id}`);
|
|
1866
|
+
}
|
|
1867
|
+
/**
|
|
1868
|
+
* Eliminar de forma permanente.
|
|
1869
|
+
* Solo disponible para SUPER_ADMIN en la organizacion origen.
|
|
1870
|
+
*/
|
|
1871
|
+
async deletePermanent(id) {
|
|
1872
|
+
validateRequired(id, "id");
|
|
1873
|
+
return this.client.delete(
|
|
1874
|
+
`${this.basePath}/${id}/permanent`
|
|
1875
|
+
);
|
|
1876
|
+
}
|
|
1877
|
+
// ==========================================
|
|
1878
|
+
// Duplicado / versiones / historial
|
|
1879
|
+
// ==========================================
|
|
1880
|
+
/** Duplicar un presupuesto sin encadenarlo como nueva version. */
|
|
1881
|
+
async duplicate(id, options) {
|
|
1882
|
+
validateRequired(id, "id");
|
|
1883
|
+
return this.client.post(
|
|
1884
|
+
`${this.basePath}/${id}/duplicate`,
|
|
1885
|
+
options ?? {}
|
|
1886
|
+
);
|
|
1887
|
+
}
|
|
1888
|
+
/** Crear una nueva version DRAFT del presupuesto, incrementando el contador. */
|
|
1889
|
+
async createVersion(id) {
|
|
1890
|
+
validateRequired(id, "id");
|
|
1891
|
+
return this.client.post(`${this.basePath}/${id}/new-version`);
|
|
1892
|
+
}
|
|
1893
|
+
/** Historial de versiones y cambios. */
|
|
1894
|
+
async getHistory(id) {
|
|
1895
|
+
validateRequired(id, "id");
|
|
1896
|
+
return this.client.get(
|
|
1897
|
+
`${this.basePath}/${id}/history`
|
|
1898
|
+
);
|
|
1899
|
+
}
|
|
1900
|
+
// ==========================================
|
|
1901
|
+
// Listado por entidad relacionada
|
|
1902
|
+
// ==========================================
|
|
1903
|
+
/** Presupuestos de un cliente. */
|
|
1904
|
+
async listByClient(clientId) {
|
|
1905
|
+
validateRequired(clientId, "clientId");
|
|
1906
|
+
return this.client.get(
|
|
1907
|
+
`${this.basePath}/by-entity/client/${clientId}`
|
|
1908
|
+
);
|
|
1909
|
+
}
|
|
1910
|
+
/** Presupuestos de un proyecto. */
|
|
1911
|
+
async listByProject(projectId) {
|
|
1912
|
+
validateRequired(projectId, "projectId");
|
|
1913
|
+
return this.client.get(
|
|
1914
|
+
`${this.basePath}/by-entity/project/${projectId}`
|
|
1915
|
+
);
|
|
1916
|
+
}
|
|
1917
|
+
/** Presupuestos de una oportunidad. */
|
|
1918
|
+
async listByOpportunity(opportunityId) {
|
|
1919
|
+
validateRequired(opportunityId, "opportunityId");
|
|
1920
|
+
return this.client.get(
|
|
1921
|
+
`${this.basePath}/by-entity/opportunity/${opportunityId}`
|
|
1922
|
+
);
|
|
1923
|
+
}
|
|
1924
|
+
// ==========================================
|
|
1925
|
+
// Items
|
|
1926
|
+
// ==========================================
|
|
1927
|
+
/** Anyadir un item al presupuesto. */
|
|
1928
|
+
async addItem(budgetId, data) {
|
|
1929
|
+
validateRequired(budgetId, "budgetId");
|
|
1930
|
+
validateRequired(data.name, "name");
|
|
1931
|
+
return this.client.post(
|
|
1932
|
+
`${this.basePath}/${budgetId}/items`,
|
|
1933
|
+
data
|
|
1934
|
+
);
|
|
1935
|
+
}
|
|
1936
|
+
/** Actualizar un item. */
|
|
1937
|
+
async updateItem(budgetId, itemId, data) {
|
|
1938
|
+
validateRequired(budgetId, "budgetId");
|
|
1939
|
+
validateRequired(itemId, "itemId");
|
|
1940
|
+
return this.client.patch(
|
|
1941
|
+
`${this.basePath}/${budgetId}/items/${itemId}`,
|
|
1942
|
+
data
|
|
1943
|
+
);
|
|
1944
|
+
}
|
|
1945
|
+
/** Eliminar un item. */
|
|
1946
|
+
async removeItem(budgetId, itemId) {
|
|
1947
|
+
validateRequired(budgetId, "budgetId");
|
|
1948
|
+
validateRequired(itemId, "itemId");
|
|
1949
|
+
return this.client.delete(
|
|
1950
|
+
`${this.basePath}/${budgetId}/items/${itemId}`
|
|
1951
|
+
);
|
|
1952
|
+
}
|
|
1953
|
+
/** Aplicar un nuevo orden a los items del presupuesto. */
|
|
1954
|
+
async reorderItems(budgetId, data) {
|
|
1955
|
+
validateRequired(budgetId, "budgetId");
|
|
1956
|
+
return this.client.patch(
|
|
1957
|
+
`${this.basePath}/${budgetId}/items/reorder`,
|
|
1958
|
+
data
|
|
1959
|
+
);
|
|
1960
|
+
}
|
|
1961
|
+
// ==========================================
|
|
1962
|
+
// Secciones
|
|
1963
|
+
// ==========================================
|
|
1964
|
+
/** Listar secciones del presupuesto ordenadas por posicion y orden. */
|
|
1965
|
+
async listSections(budgetId) {
|
|
1966
|
+
validateRequired(budgetId, "budgetId");
|
|
1967
|
+
return this.client.get(
|
|
1968
|
+
`${this.basePath}/${budgetId}/sections`
|
|
1969
|
+
);
|
|
1970
|
+
}
|
|
1971
|
+
/** Anyadir una seccion al presupuesto. */
|
|
1972
|
+
async addSection(budgetId, data) {
|
|
1973
|
+
validateRequired(budgetId, "budgetId");
|
|
1974
|
+
validateRequired(data.title, "title");
|
|
1975
|
+
return this.client.post(
|
|
1976
|
+
`${this.basePath}/${budgetId}/sections`,
|
|
1977
|
+
data
|
|
1978
|
+
);
|
|
1979
|
+
}
|
|
1980
|
+
/** Actualizar una seccion existente. */
|
|
1981
|
+
async updateSection(budgetId, sectionId, data) {
|
|
1982
|
+
validateRequired(budgetId, "budgetId");
|
|
1983
|
+
validateRequired(sectionId, "sectionId");
|
|
1984
|
+
return this.client.patch(
|
|
1985
|
+
`${this.basePath}/${budgetId}/sections/${sectionId}`,
|
|
1986
|
+
data
|
|
1987
|
+
);
|
|
1988
|
+
}
|
|
1989
|
+
/** Eliminar una seccion. */
|
|
1990
|
+
async removeSection(budgetId, sectionId) {
|
|
1991
|
+
validateRequired(budgetId, "budgetId");
|
|
1992
|
+
validateRequired(sectionId, "sectionId");
|
|
1993
|
+
return this.client.delete(
|
|
1994
|
+
`${this.basePath}/${budgetId}/sections/${sectionId}`
|
|
1995
|
+
);
|
|
1996
|
+
}
|
|
1997
|
+
/** Reordenar secciones. */
|
|
1998
|
+
async reorderSections(budgetId, data) {
|
|
1999
|
+
validateRequired(budgetId, "budgetId");
|
|
2000
|
+
return this.client.patch(
|
|
2001
|
+
`${this.basePath}/${budgetId}/sections/reorder`,
|
|
2002
|
+
data
|
|
2003
|
+
);
|
|
2004
|
+
}
|
|
2005
|
+
// ==========================================
|
|
2006
|
+
// Acciones de flujo
|
|
2007
|
+
// ==========================================
|
|
2008
|
+
/** Enviar el presupuesto por correo al cliente. */
|
|
2009
|
+
async send(id, data) {
|
|
2010
|
+
validateRequired(id, "id");
|
|
2011
|
+
if (!Array.isArray(data.emails) || data.emails.length === 0) {
|
|
2012
|
+
throw new Error("Debes indicar al menos un email destinatario");
|
|
2013
|
+
}
|
|
2014
|
+
return this.client.post(`${this.basePath}/${id}/send`, data);
|
|
2015
|
+
}
|
|
2016
|
+
/** Aprobar el presupuesto. */
|
|
2017
|
+
async approve(id, data = {}) {
|
|
2018
|
+
validateRequired(id, "id");
|
|
2019
|
+
return this.client.post(`${this.basePath}/${id}/approve`, data);
|
|
2020
|
+
}
|
|
2021
|
+
/** Rechazar el presupuesto. El motivo (reason) es obligatorio. */
|
|
2022
|
+
async reject(id, data) {
|
|
2023
|
+
validateRequired(id, "id");
|
|
2024
|
+
validateRequired(data.reason, "reason");
|
|
2025
|
+
return this.client.post(`${this.basePath}/${id}/reject`, data);
|
|
2026
|
+
}
|
|
2027
|
+
/** Cancelar el presupuesto. */
|
|
2028
|
+
async cancel(id) {
|
|
2029
|
+
validateRequired(id, "id");
|
|
2030
|
+
return this.client.post(`${this.basePath}/${id}/cancel`);
|
|
2031
|
+
}
|
|
2032
|
+
/**
|
|
2033
|
+
* Cambiar el estado del presupuesto directamente (uso administrativo).
|
|
2034
|
+
* Restringido a SUPER_ADMIN en el backend.
|
|
2035
|
+
*/
|
|
2036
|
+
async changeStatus(id, status, reason) {
|
|
2037
|
+
validateRequired(id, "id");
|
|
2038
|
+
validateRequired(status, "status");
|
|
2039
|
+
const payload = { status, reason };
|
|
2040
|
+
return this.client.post(
|
|
2041
|
+
`${this.basePath}/${id}/change-status`,
|
|
2042
|
+
payload
|
|
2043
|
+
);
|
|
2044
|
+
}
|
|
2045
|
+
/** Convertir un presupuesto aprobado en un proyecto CRM. */
|
|
2046
|
+
async convertToProject(id, options) {
|
|
2047
|
+
validateRequired(id, "id");
|
|
2048
|
+
validateRequired(options.name, "name");
|
|
2049
|
+
validateRequired(options.startDate, "startDate");
|
|
2050
|
+
return this.client.post(
|
|
2051
|
+
`${this.basePath}/${id}/convert-to-project`,
|
|
2052
|
+
options
|
|
2053
|
+
);
|
|
2054
|
+
}
|
|
2055
|
+
/** Convertir un presupuesto aprobado en factura. */
|
|
2056
|
+
async convertToInvoice(id, options = {}) {
|
|
2057
|
+
validateRequired(id, "id");
|
|
2058
|
+
return this.client.post(
|
|
2059
|
+
`${this.basePath}/${id}/convert-to-invoice`,
|
|
2060
|
+
options
|
|
2061
|
+
);
|
|
2062
|
+
}
|
|
2063
|
+
// ==========================================
|
|
2064
|
+
// PDF
|
|
2065
|
+
// ==========================================
|
|
2066
|
+
/**
|
|
2067
|
+
* Generar el PDF del presupuesto para descarga.
|
|
2068
|
+
* Devuelve un Blob. Si el consumidor necesita una URL, usa `URL.createObjectURL`.
|
|
2069
|
+
*/
|
|
2070
|
+
async generatePdf(id) {
|
|
2071
|
+
validateRequired(id, "id");
|
|
2072
|
+
return this.fetchBinary(`${this.basePath}/${id}/generate-pdf`, "POST");
|
|
2073
|
+
}
|
|
2074
|
+
/**
|
|
2075
|
+
* Visualizar el PDF del presupuesto (Content-Disposition inline).
|
|
2076
|
+
* Devuelve un Blob.
|
|
2077
|
+
*/
|
|
2078
|
+
async viewPdf(id) {
|
|
2079
|
+
validateRequired(id, "id");
|
|
2080
|
+
return this.fetchBinary(`${this.basePath}/${id}/pdf`, "GET");
|
|
2081
|
+
}
|
|
2082
|
+
/**
|
|
2083
|
+
* Construir la URL del PDF del presupuesto (util para <iframe>, <embed>, etc.).
|
|
2084
|
+
* Incluye las credenciales en query string para funcionar en etiquetas HTML
|
|
2085
|
+
* que no pueden anyadir headers personalizados.
|
|
2086
|
+
*/
|
|
2087
|
+
getPdfUrl(id) {
|
|
2088
|
+
validateRequired(id, "id");
|
|
2089
|
+
return this.client.buildSseUrl(`${this.basePath}/${id}/pdf`);
|
|
2090
|
+
}
|
|
2091
|
+
// ==========================================
|
|
2092
|
+
// Adjuntos
|
|
2093
|
+
// ==========================================
|
|
2094
|
+
/** Listar adjuntos del presupuesto. */
|
|
2095
|
+
async getAttachments(budgetId) {
|
|
2096
|
+
validateRequired(budgetId, "budgetId");
|
|
2097
|
+
return this.client.get(
|
|
2098
|
+
`${this.basePath}/${budgetId}/attachments`
|
|
2099
|
+
);
|
|
2100
|
+
}
|
|
2101
|
+
/**
|
|
2102
|
+
* Subir un adjunto al presupuesto.
|
|
2103
|
+
*
|
|
2104
|
+
* @param budgetId - UUID del presupuesto
|
|
2105
|
+
* @param file - Archivo a subir (File o Blob)
|
|
2106
|
+
* @param meta - Nombre visible y descripcion opcionales
|
|
2107
|
+
*/
|
|
2108
|
+
async uploadAttachment(budgetId, file, meta) {
|
|
2109
|
+
validateRequired(budgetId, "budgetId");
|
|
2110
|
+
const formData = new FormData();
|
|
2111
|
+
if (file instanceof Blob && !(file instanceof File)) {
|
|
2112
|
+
formData.append("file", file, meta?.name || "attachment");
|
|
2113
|
+
} else {
|
|
2114
|
+
formData.append("file", file);
|
|
2115
|
+
}
|
|
2116
|
+
if (meta?.name) formData.append("name", meta.name);
|
|
2117
|
+
if (meta?.description) formData.append("description", meta.description);
|
|
2118
|
+
return this.client.postForm(
|
|
2119
|
+
`${this.basePath}/${budgetId}/attachments`,
|
|
2120
|
+
formData
|
|
2121
|
+
);
|
|
2122
|
+
}
|
|
2123
|
+
/** Eliminar un adjunto del presupuesto. */
|
|
2124
|
+
async deleteAttachment(budgetId, attachmentId) {
|
|
2125
|
+
validateRequired(budgetId, "budgetId");
|
|
2126
|
+
validateRequired(attachmentId, "attachmentId");
|
|
2127
|
+
return this.client.delete(
|
|
2128
|
+
`${this.basePath}/${budgetId}/attachments/${attachmentId}`
|
|
2129
|
+
);
|
|
2130
|
+
}
|
|
2131
|
+
/**
|
|
2132
|
+
* Subir una imagen para el editor WYSIWYG.
|
|
2133
|
+
* La imagen no aparece como adjunto en el File Manager: solo devuelve la URL
|
|
2134
|
+
* para incrustarla en el contenido del presupuesto.
|
|
2135
|
+
*/
|
|
2136
|
+
async uploadEditorImage(budgetId, file) {
|
|
2137
|
+
validateRequired(budgetId, "budgetId");
|
|
2138
|
+
const formData = new FormData();
|
|
2139
|
+
if (file instanceof Blob && !(file instanceof File)) {
|
|
2140
|
+
formData.append("file", file, "image");
|
|
2141
|
+
} else {
|
|
2142
|
+
formData.append("file", file);
|
|
2143
|
+
}
|
|
2144
|
+
return this.client.postForm(
|
|
2145
|
+
`${this.basePath}/${budgetId}/editor-images`,
|
|
2146
|
+
formData
|
|
2147
|
+
);
|
|
2148
|
+
}
|
|
2149
|
+
// ==========================================
|
|
2150
|
+
// Utilidades: codigo, stats, export, masivos
|
|
2151
|
+
// ==========================================
|
|
2152
|
+
/** Proximo codigo sugerido segun la configuracion de numeracion. */
|
|
2153
|
+
async getNextCode() {
|
|
2154
|
+
return this.client.get(`${this.basePath}/next-code`);
|
|
2155
|
+
}
|
|
2156
|
+
/** Comprobar si un codigo ya esta en uso. */
|
|
2157
|
+
async checkCode(code, excludeId) {
|
|
2158
|
+
validateRequired(code, "code");
|
|
2159
|
+
return this.client.get(
|
|
2160
|
+
`${this.basePath}/check-code/${encodeURIComponent(code)}`,
|
|
2161
|
+
{ params: excludeId ? { excludeId } : void 0 }
|
|
2162
|
+
);
|
|
2163
|
+
}
|
|
2164
|
+
/** Estadisticas agregadas (totales, tasa aprobacion, etc.). */
|
|
2165
|
+
async getStats(filters) {
|
|
2166
|
+
const params = this.serializeFilters(filters);
|
|
2167
|
+
return this.client.get(`${this.basePath}/stats`, {
|
|
2168
|
+
params
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
2171
|
+
/** Ejecutar una operacion masiva (delete / cancel / change-status). */
|
|
2172
|
+
async bulk(operation, ids, options) {
|
|
2173
|
+
if (!Array.isArray(ids) || ids.length === 0) {
|
|
2174
|
+
throw new Error("Debes indicar al menos un id de presupuesto");
|
|
2175
|
+
}
|
|
2176
|
+
const payload = { operation, ids, options };
|
|
2177
|
+
return this.client.post(`${this.basePath}/bulk`, payload);
|
|
2178
|
+
}
|
|
2179
|
+
/**
|
|
2180
|
+
* Exportar el listado filtrado en el formato indicado.
|
|
2181
|
+
* De momento solo se soporta `csv`. Devuelve un Blob.
|
|
2182
|
+
*/
|
|
2183
|
+
async export(filters, format = "csv") {
|
|
2184
|
+
if (format !== "csv") {
|
|
2185
|
+
throw new Error(`Formato de exportacion no soportado: ${format}`);
|
|
2186
|
+
}
|
|
2187
|
+
const params = this.serializeFilters(filters);
|
|
2188
|
+
return this.fetchBinary(`${this.basePath}/export`, "GET", params);
|
|
2189
|
+
}
|
|
2190
|
+
// ==========================================
|
|
2191
|
+
// IA
|
|
2192
|
+
// ==========================================
|
|
2193
|
+
/**
|
|
2194
|
+
* Encolar un job asincrono para generar items del presupuesto a partir
|
|
2195
|
+
* de un texto en lenguaje natural. Devuelve el `jobId` para seguir el
|
|
2196
|
+
* progreso vía `streamGenerateItemsStatus`.
|
|
2197
|
+
*/
|
|
2198
|
+
async generateItemsWithAi(data) {
|
|
2199
|
+
validateRequired(data.text, "text");
|
|
2200
|
+
validateStringLength(data.text, "text", 10);
|
|
2201
|
+
return this.client.post(
|
|
2202
|
+
`${this.basePath}/generate-items-ai`,
|
|
2203
|
+
data
|
|
2204
|
+
);
|
|
2205
|
+
}
|
|
2206
|
+
/**
|
|
2207
|
+
* Suscribirse al progreso (SSE) del job de generacion de items con IA.
|
|
2208
|
+
* Requiere EventSource en el entorno.
|
|
2209
|
+
*/
|
|
2210
|
+
streamGenerateItemsStatus(jobId, options) {
|
|
2211
|
+
validateRequired(jobId, "jobId");
|
|
2212
|
+
if (typeof EventSource === "undefined") {
|
|
2213
|
+
throw new Error(
|
|
2214
|
+
"EventSource no esta disponible en este entorno. Usa un polyfill en Node."
|
|
2215
|
+
);
|
|
2216
|
+
}
|
|
2217
|
+
const sseUrl = this.client.buildSseUrl(
|
|
2218
|
+
`${this.basePath}/generate-items-ai/${jobId}/status`
|
|
2219
|
+
);
|
|
2220
|
+
const es = new EventSource(sseUrl);
|
|
2221
|
+
es.addEventListener("progress", (event) => {
|
|
2222
|
+
try {
|
|
2223
|
+
const data = JSON.parse(event.data);
|
|
2224
|
+
options?.onProgress?.(data);
|
|
2225
|
+
} catch {
|
|
2226
|
+
}
|
|
2227
|
+
});
|
|
2228
|
+
es.addEventListener("complete", (event) => {
|
|
2229
|
+
try {
|
|
2230
|
+
const data = JSON.parse(event.data);
|
|
2231
|
+
options?.onComplete?.(data);
|
|
2232
|
+
} catch {
|
|
2233
|
+
} finally {
|
|
2234
|
+
es.close();
|
|
2235
|
+
}
|
|
2236
|
+
});
|
|
2237
|
+
es.addEventListener("error", (event) => {
|
|
2238
|
+
try {
|
|
2239
|
+
const data = event.data ? JSON.parse(event.data) : null;
|
|
2240
|
+
options?.onError?.(
|
|
2241
|
+
new Error(data?.error || "Error en el job de generacion de items")
|
|
2242
|
+
);
|
|
2243
|
+
} catch {
|
|
2244
|
+
options?.onError?.(new Error("Error en la conexion SSE"));
|
|
2245
|
+
} finally {
|
|
2246
|
+
es.close();
|
|
2247
|
+
}
|
|
2248
|
+
});
|
|
2249
|
+
return { close: () => es.close() };
|
|
2250
|
+
}
|
|
2251
|
+
/**
|
|
2252
|
+
* Generar un presupuesto completo (secciones + items) a partir de un texto.
|
|
2253
|
+
* Devuelve la estructura sugerida sin crear el presupuesto.
|
|
2254
|
+
*/
|
|
2255
|
+
async generateCompleteWithAi(data) {
|
|
2256
|
+
validateRequired(data.text, "text");
|
|
2257
|
+
validateStringLength(data.text, "text", 10);
|
|
2258
|
+
return this.client.post(
|
|
2259
|
+
`${this.basePath}/generate-complete-ai`,
|
|
2260
|
+
data
|
|
2261
|
+
);
|
|
2262
|
+
}
|
|
2263
|
+
/** Generar unicamente secciones sugeridas para un presupuesto existente. */
|
|
2264
|
+
async generateSectionsWithAi(data) {
|
|
2265
|
+
validateRequired(data.text, "text");
|
|
2266
|
+
validateStringLength(data.text, "text", 10);
|
|
2267
|
+
return this.client.post(
|
|
2268
|
+
`${this.basePath}/generate-sections-ai`,
|
|
2269
|
+
data
|
|
2270
|
+
);
|
|
2271
|
+
}
|
|
2272
|
+
// ==========================================
|
|
2273
|
+
// Helpers privados
|
|
2274
|
+
// ==========================================
|
|
2275
|
+
/**
|
|
2276
|
+
* Convierte los filtros a un objeto plano apto para query string.
|
|
2277
|
+
* Aplana arrays en formato coma-separado (patron del backend).
|
|
2278
|
+
*/
|
|
2279
|
+
serializeFilters(filters) {
|
|
2280
|
+
if (!filters) return void 0;
|
|
2281
|
+
const out = {};
|
|
2282
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
2283
|
+
if (value === void 0 || value === null) continue;
|
|
2284
|
+
if (Array.isArray(value)) {
|
|
2285
|
+
out[key] = value.join(",");
|
|
2286
|
+
} else {
|
|
2287
|
+
out[key] = value;
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
return out;
|
|
2291
|
+
}
|
|
2292
|
+
/**
|
|
2293
|
+
* Descargar una respuesta binaria (PDF, CSV) respetando el auth del cliente.
|
|
2294
|
+
* Usa `buildSseUrl` para añadir las credenciales en la URL cuando la ruta
|
|
2295
|
+
* no pueda llevarlas en headers (p. ej. etiquetas `<a href>`).
|
|
2296
|
+
*/
|
|
2297
|
+
async fetchBinary(path, method, params) {
|
|
2298
|
+
if (method === "GET") {
|
|
2299
|
+
const response2 = await this.client.get(path, {
|
|
2300
|
+
params,
|
|
2301
|
+
responseType: "arraybuffer"
|
|
2302
|
+
});
|
|
2303
|
+
return new Blob([response2]);
|
|
2304
|
+
}
|
|
2305
|
+
const response = await this.client.post(path, void 0, {
|
|
2306
|
+
params,
|
|
2307
|
+
responseType: "arraybuffer"
|
|
2308
|
+
});
|
|
2309
|
+
return new Blob([response]);
|
|
2310
|
+
}
|
|
2311
|
+
};
|
|
2312
|
+
|
|
2313
|
+
// src/services/budget-templates.service.ts
|
|
2314
|
+
var BudgetTemplatesService = class {
|
|
2315
|
+
constructor(client) {
|
|
2316
|
+
this.basePath = "/crm/budget-templates";
|
|
2317
|
+
this.budgetsPath = "/crm/budgets";
|
|
2318
|
+
this.client = client;
|
|
2319
|
+
}
|
|
2320
|
+
/**
|
|
2321
|
+
* Listar plantillas activas.
|
|
2322
|
+
* Las plantillas del sistema aparecen primero, seguidas de las
|
|
2323
|
+
* personalizadas ordenadas por fecha de creacion descendente.
|
|
2324
|
+
*/
|
|
2325
|
+
async list() {
|
|
2326
|
+
return this.client.get(this.basePath);
|
|
2327
|
+
}
|
|
2328
|
+
/** Obtener el detalle de una plantilla por UUID. */
|
|
2329
|
+
async get(id) {
|
|
2330
|
+
validateRequired(id, "id");
|
|
2331
|
+
return this.client.get(`${this.basePath}/${id}`);
|
|
2332
|
+
}
|
|
2333
|
+
/** Crear una plantilla personalizada. */
|
|
2334
|
+
async create(data) {
|
|
2335
|
+
validateRequired(data.name, "name");
|
|
2336
|
+
validateStringLength(data.name, "name", 1, 100);
|
|
2337
|
+
return this.client.post(this.basePath, data);
|
|
2338
|
+
}
|
|
2339
|
+
/** Actualizar una plantilla personalizada (las del sistema son inmutables). */
|
|
2340
|
+
async update(id, data) {
|
|
2341
|
+
validateRequired(id, "id");
|
|
2342
|
+
return this.client.patch(`${this.basePath}/${id}`, data);
|
|
2343
|
+
}
|
|
2344
|
+
/** Archivar (soft delete) una plantilla personalizada. */
|
|
2345
|
+
async delete(id) {
|
|
2346
|
+
validateRequired(id, "id");
|
|
2347
|
+
return this.client.delete(`${this.basePath}/${id}`);
|
|
2348
|
+
}
|
|
2349
|
+
/**
|
|
2350
|
+
* Crear una plantilla personalizada copiando items y secciones de un
|
|
2351
|
+
* presupuesto existente.
|
|
2352
|
+
*/
|
|
2353
|
+
async createFromBudget(budgetId, data) {
|
|
2354
|
+
validateRequired(budgetId, "budgetId");
|
|
2355
|
+
validateRequired(data.name, "name");
|
|
2356
|
+
return this.client.post(
|
|
2357
|
+
`${this.basePath}/from-budget/${budgetId}`,
|
|
2358
|
+
data
|
|
2359
|
+
);
|
|
2360
|
+
}
|
|
2361
|
+
/**
|
|
2362
|
+
* Crear un presupuesto DRAFT aplicando una plantilla.
|
|
2363
|
+
* Copia los items/secciones de la plantilla y asigna cliente/oportunidad.
|
|
2364
|
+
*/
|
|
2365
|
+
async applyTemplate(templateId, data) {
|
|
2366
|
+
validateRequired(templateId, "templateId");
|
|
2367
|
+
validateRequired(data.clientId, "clientId");
|
|
2368
|
+
return this.client.post(
|
|
2369
|
+
`${this.budgetsPath}/from-template/${templateId}`,
|
|
2370
|
+
data
|
|
2371
|
+
);
|
|
2372
|
+
}
|
|
2373
|
+
};
|
|
2374
|
+
|
|
2375
|
+
// src/services/budget-comments.service.ts
|
|
2376
|
+
var BudgetCommentsService = class {
|
|
2377
|
+
constructor(client) {
|
|
2378
|
+
this.basePath = "/crm/budgets";
|
|
2379
|
+
this.client = client;
|
|
2380
|
+
}
|
|
2381
|
+
/**
|
|
2382
|
+
* Listar comentarios del presupuesto.
|
|
2383
|
+
*
|
|
2384
|
+
* @param budgetId - UUID del presupuesto.
|
|
2385
|
+
* @param filter - Filtros de visibilidad, paginación y rango de fechas.
|
|
2386
|
+
*
|
|
2387
|
+
* @example
|
|
2388
|
+
* ```typescript
|
|
2389
|
+
* const page = await sdk.budgetComments.list(budgetId, { visibility: 'CLIENT', page: 1, limit: 20 });
|
|
2390
|
+
* page.data.forEach(c => console.log(c.body));
|
|
2391
|
+
* ```
|
|
2392
|
+
*/
|
|
2393
|
+
async list(budgetId, filter) {
|
|
2394
|
+
validateRequired(budgetId, "budgetId");
|
|
2395
|
+
const params = this.serializeListFilter(filter);
|
|
2396
|
+
const raw = await this.client.get(
|
|
2397
|
+
`${this.basePath}/${budgetId}/comments`,
|
|
2398
|
+
{ params }
|
|
2399
|
+
);
|
|
2400
|
+
const data = raw.data ?? raw.items ?? [];
|
|
2401
|
+
const pageInfo = {
|
|
2402
|
+
...raw.pagination ?? {},
|
|
2403
|
+
...raw.pageInfo ?? {}
|
|
2404
|
+
};
|
|
2405
|
+
return { data, pageInfo };
|
|
2406
|
+
}
|
|
2407
|
+
/**
|
|
2408
|
+
* Obtener un comentario por su ID.
|
|
2409
|
+
*
|
|
2410
|
+
* Nota: el backend actual no expone `GET /comments/:commentId` a nivel de
|
|
2411
|
+
* REST. El SDK lo emula filtrando localmente el resultado del listado. Si
|
|
2412
|
+
* en el futuro se añade un endpoint dedicado, este método se puede migrar
|
|
2413
|
+
* sin romper la firma.
|
|
2414
|
+
*/
|
|
2415
|
+
async get(budgetId, commentId) {
|
|
2416
|
+
validateRequired(budgetId, "budgetId");
|
|
2417
|
+
validateRequired(commentId, "commentId");
|
|
2418
|
+
const page = await this.list(budgetId, { limit: 100 });
|
|
2419
|
+
const found = page.data.find((c) => c.id === commentId);
|
|
2420
|
+
if (!found) {
|
|
2421
|
+
throw new Error(
|
|
2422
|
+
`Comentario ${commentId} no encontrado en el presupuesto ${budgetId}`
|
|
2423
|
+
);
|
|
2424
|
+
}
|
|
2425
|
+
return found;
|
|
2426
|
+
}
|
|
2427
|
+
/**
|
|
2428
|
+
* Crear un comentario en el presupuesto.
|
|
2429
|
+
*
|
|
2430
|
+
* La visibilidad (INTERNAL / CLIENT) debe ser compatible con los permisos
|
|
2431
|
+
* del usuario autenticado. Las menciones solo tienen efecto en comentarios
|
|
2432
|
+
* internos.
|
|
2433
|
+
*/
|
|
2434
|
+
async create(budgetId, input) {
|
|
2435
|
+
validateRequired(budgetId, "budgetId");
|
|
2436
|
+
validateRequired(input.body, "body");
|
|
2437
|
+
validateStringLength(input.body, "body", 1, 5e3);
|
|
2438
|
+
return this.client.post(
|
|
2439
|
+
`${this.basePath}/${budgetId}/comments`,
|
|
2440
|
+
input
|
|
2441
|
+
);
|
|
2442
|
+
}
|
|
2443
|
+
/** Actualizar el cuerpo de un comentario. Solo autor o SUPER_ADMIN. */
|
|
2444
|
+
async update(budgetId, commentId, input) {
|
|
2445
|
+
validateRequired(budgetId, "budgetId");
|
|
2446
|
+
validateRequired(commentId, "commentId");
|
|
2447
|
+
validateRequired(input.body, "body");
|
|
2448
|
+
validateStringLength(input.body, "body", 1, 5e3);
|
|
2449
|
+
return this.client.patch(
|
|
2450
|
+
`${this.basePath}/${budgetId}/comments/${commentId}`,
|
|
2451
|
+
input
|
|
2452
|
+
);
|
|
2453
|
+
}
|
|
2454
|
+
/** Eliminar un comentario. Solo autor o SUPER_ADMIN. */
|
|
2455
|
+
async remove(budgetId, commentId) {
|
|
2456
|
+
validateRequired(budgetId, "budgetId");
|
|
2457
|
+
validateRequired(commentId, "commentId");
|
|
2458
|
+
return this.client.delete(
|
|
2459
|
+
`${this.basePath}/${budgetId}/comments/${commentId}`
|
|
2460
|
+
);
|
|
2461
|
+
}
|
|
2462
|
+
/**
|
|
2463
|
+
* Creación masiva tolerante a fallos (saga).
|
|
2464
|
+
*
|
|
2465
|
+
* Nota: a día de hoy, el backend expone el bulk únicamente a través de la
|
|
2466
|
+
* herramienta MCP `crm_budget_comments.bulk_create`. Para consumirlo desde
|
|
2467
|
+
* REST, el SDK llama secuencialmente a `create` y agrega el resultado,
|
|
2468
|
+
* respetando el contrato del método. Sustituir cuando exista un endpoint
|
|
2469
|
+
* REST dedicado.
|
|
2470
|
+
*/
|
|
2471
|
+
async bulkCreate(budgetId, items, options) {
|
|
2472
|
+
validateRequired(budgetId, "budgetId");
|
|
2473
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
2474
|
+
throw new Error("Debes indicar al menos un comentario en items");
|
|
2475
|
+
}
|
|
2476
|
+
if (items.length > 50) {
|
|
2477
|
+
throw new Error("No se pueden crear m\xE1s de 50 comentarios por lote");
|
|
2478
|
+
}
|
|
2479
|
+
if (options?.idempotencyKey) {
|
|
2480
|
+
try {
|
|
2481
|
+
const raw = await this.client.post(
|
|
2482
|
+
`${this.basePath}/${budgetId}/comments:bulk`,
|
|
2483
|
+
{ items },
|
|
2484
|
+
{ headers: { "Idempotency-Key": options.idempotencyKey } }
|
|
2485
|
+
);
|
|
2486
|
+
return this.normalizeBulkResult(raw);
|
|
2487
|
+
} catch {
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
const succeeded = [];
|
|
2491
|
+
const failed = [];
|
|
2492
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
2493
|
+
const dto = items[index];
|
|
2494
|
+
try {
|
|
2495
|
+
const created = await this.create(budgetId, dto);
|
|
2496
|
+
succeeded.push(created);
|
|
2497
|
+
} catch (error) {
|
|
2498
|
+
const message = error instanceof Error ? error.message : "Error desconocido";
|
|
2499
|
+
failed.push({
|
|
2500
|
+
index,
|
|
2501
|
+
clientOperationId: dto.clientOperationId,
|
|
2502
|
+
code: this.mapErrorCode(error),
|
|
2503
|
+
message
|
|
2504
|
+
});
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
return {
|
|
2508
|
+
succeeded,
|
|
2509
|
+
failed,
|
|
2510
|
+
summary: {
|
|
2511
|
+
total: items.length,
|
|
2512
|
+
ok: succeeded.length,
|
|
2513
|
+
failed: failed.length
|
|
2514
|
+
}
|
|
2515
|
+
};
|
|
2516
|
+
}
|
|
2517
|
+
/**
|
|
2518
|
+
* Devuelve el timeline unificado: comentarios + eventos de historial del
|
|
2519
|
+
* presupuesto ordenados cronológicamente descendente.
|
|
2520
|
+
*
|
|
2521
|
+
* Nota: el backend expone esta vista agregada a través de la herramienta MCP
|
|
2522
|
+
* `crm_budget_comments.get_timeline`. Hasta que se habilite un endpoint REST
|
|
2523
|
+
* dedicado, el SDK devuelve los comentarios paginados y un `items` sólo con
|
|
2524
|
+
* kind `COMMENT`. Se marca como follow-up.
|
|
2525
|
+
*/
|
|
2526
|
+
async getTimeline(budgetId, filter) {
|
|
2527
|
+
validateRequired(budgetId, "budgetId");
|
|
2528
|
+
try {
|
|
2529
|
+
const params = this.serializeTimelineFilter(filter);
|
|
2530
|
+
const raw = await this.client.get(
|
|
2531
|
+
`${this.basePath}/${budgetId}/timeline`,
|
|
2532
|
+
{ params }
|
|
2533
|
+
);
|
|
2534
|
+
return raw;
|
|
2535
|
+
} catch {
|
|
2536
|
+
const page = await this.list(budgetId, {
|
|
2537
|
+
limit: filter?.limit ?? 50,
|
|
2538
|
+
since: filter?.since,
|
|
2539
|
+
until: filter?.until
|
|
2540
|
+
});
|
|
2541
|
+
return {
|
|
2542
|
+
items: page.data.map((comment) => ({
|
|
2543
|
+
kind: "COMMENT",
|
|
2544
|
+
at: comment.createdAt,
|
|
2545
|
+
comment
|
|
2546
|
+
})),
|
|
2547
|
+
pageInfo: {
|
|
2548
|
+
nextCursor: page.pageInfo?.nextCursor ?? null,
|
|
2549
|
+
hasMore: page.pageInfo?.hasMore ?? false,
|
|
2550
|
+
pageSize: page.data.length
|
|
2551
|
+
}
|
|
2552
|
+
};
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
// ==========================================
|
|
2556
|
+
// Helpers privados
|
|
2557
|
+
// ==========================================
|
|
2558
|
+
serializeListFilter(filter) {
|
|
2559
|
+
if (!filter) return void 0;
|
|
2560
|
+
const out = {};
|
|
2561
|
+
if (filter.visibility && filter.visibility !== "ALL") {
|
|
2562
|
+
out.visibility = filter.visibility;
|
|
2563
|
+
}
|
|
2564
|
+
if (filter.authorId) out.authorId = filter.authorId;
|
|
2565
|
+
if (filter.mentionedUserId) out.mentionedUserId = filter.mentionedUserId;
|
|
2566
|
+
if (filter.cursor) out.cursor = filter.cursor;
|
|
2567
|
+
if (typeof filter.limit === "number") out.limit = filter.limit;
|
|
2568
|
+
if (typeof filter.page === "number") out.page = filter.page;
|
|
2569
|
+
if (filter.since) out.since = filter.since;
|
|
2570
|
+
if (filter.until) out.until = filter.until;
|
|
2571
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
2572
|
+
}
|
|
2573
|
+
serializeTimelineFilter(filter) {
|
|
2574
|
+
if (!filter) return void 0;
|
|
2575
|
+
const out = {};
|
|
2576
|
+
if (filter.cursor) out.cursor = filter.cursor;
|
|
2577
|
+
if (typeof filter.limit === "number") out.limit = filter.limit;
|
|
2578
|
+
if (filter.since) out.since = filter.since;
|
|
2579
|
+
if (filter.until) out.until = filter.until;
|
|
2580
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
2581
|
+
}
|
|
2582
|
+
normalizeBulkResult(raw) {
|
|
2583
|
+
const total = raw.summary?.total ?? raw.succeeded.length + raw.failed.length;
|
|
2584
|
+
const ok = raw.summary?.ok ?? raw.succeeded.length;
|
|
2585
|
+
const failed = raw.summary?.failed ?? raw.failed.length;
|
|
2586
|
+
return {
|
|
2587
|
+
succeeded: raw.succeeded,
|
|
2588
|
+
failed: raw.failed,
|
|
2589
|
+
summary: { total, ok, failed }
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2592
|
+
mapErrorCode(error) {
|
|
2593
|
+
if (!error || typeof error !== "object") return "INTERNAL";
|
|
2594
|
+
const code = error.code;
|
|
2595
|
+
if (typeof code === "string") return code;
|
|
2596
|
+
const statusCode = error.statusCode;
|
|
2597
|
+
if (statusCode === 404) return "NOT_FOUND";
|
|
2598
|
+
if (statusCode === 403) return "FORBIDDEN";
|
|
2599
|
+
if (statusCode === 400) return "VALIDATION";
|
|
2600
|
+
if (statusCode === 409) return "CONFLICT";
|
|
2601
|
+
if (statusCode === 429) return "RATE_LIMITED";
|
|
2602
|
+
return "INTERNAL";
|
|
2603
|
+
}
|
|
2604
|
+
};
|
|
2605
|
+
|
|
2606
|
+
// src/services/budget-signatures.service.ts
|
|
2607
|
+
var BudgetSignaturesService = class {
|
|
2608
|
+
constructor(client) {
|
|
2609
|
+
this.basePath = "/crm/budgets";
|
|
2610
|
+
this.client = client;
|
|
2611
|
+
}
|
|
2612
|
+
// ==========================================
|
|
2613
|
+
// Firmas registradas
|
|
2614
|
+
// ==========================================
|
|
2615
|
+
/** Listar todas las firmas asociadas al presupuesto. */
|
|
2616
|
+
async list(budgetId) {
|
|
2617
|
+
validateRequired(budgetId, "budgetId");
|
|
2618
|
+
const raw = await this.client.get(
|
|
2619
|
+
`${this.basePath}/${budgetId}/signatures`
|
|
2620
|
+
);
|
|
2621
|
+
return raw.items ?? [];
|
|
2622
|
+
}
|
|
2623
|
+
/** Obtener el detalle de una firma concreta. */
|
|
2624
|
+
async get(budgetId, signatureId) {
|
|
2625
|
+
validateRequired(budgetId, "budgetId");
|
|
2626
|
+
validateRequired(signatureId, "signatureId");
|
|
2627
|
+
return this.client.get(
|
|
2628
|
+
`${this.basePath}/${budgetId}/signatures/${signatureId}`
|
|
2629
|
+
);
|
|
2630
|
+
}
|
|
2631
|
+
/**
|
|
2632
|
+
* Verificar la integridad de una firma. Recalcula el hash del documento con
|
|
2633
|
+
* la versión (`hashVersion`) con la que se firmó originalmente y lo compara.
|
|
2634
|
+
*
|
|
2635
|
+
* Si `valid === false`, el documento ha sido modificado después de firmar.
|
|
2636
|
+
*/
|
|
2637
|
+
async verify(budgetId, signatureId) {
|
|
2638
|
+
validateRequired(budgetId, "budgetId");
|
|
2639
|
+
validateRequired(signatureId, "signatureId");
|
|
2640
|
+
return this.client.get(
|
|
2641
|
+
`${this.basePath}/${budgetId}/signatures/${signatureId}/verify`
|
|
2642
|
+
);
|
|
2643
|
+
}
|
|
2644
|
+
/**
|
|
2645
|
+
* Descargar el certificado legal de una firma como Blob.
|
|
2646
|
+
*
|
|
2647
|
+
* Ideal para consumidores que necesiten guardar el PDF localmente o abrirlo
|
|
2648
|
+
* con `URL.createObjectURL`.
|
|
2649
|
+
*/
|
|
2650
|
+
async downloadCertificate(budgetId, signatureId) {
|
|
2651
|
+
validateRequired(budgetId, "budgetId");
|
|
2652
|
+
validateRequired(signatureId, "signatureId");
|
|
2653
|
+
const response = await this.client.get(
|
|
2654
|
+
`${this.basePath}/${budgetId}/signatures/${signatureId}/certificate.pdf`,
|
|
2655
|
+
{ responseType: "arraybuffer" }
|
|
2656
|
+
);
|
|
2657
|
+
return new Blob([response], { type: "application/pdf" });
|
|
2658
|
+
}
|
|
2659
|
+
/**
|
|
2660
|
+
* Construir la URL del certificado (útil para `<iframe>` o enlaces directos).
|
|
2661
|
+
* Incluye las credenciales en query string cuando el SDK usa API key.
|
|
2662
|
+
*/
|
|
2663
|
+
getCertificateUrl(budgetId, signatureId) {
|
|
2664
|
+
validateRequired(budgetId, "budgetId");
|
|
2665
|
+
validateRequired(signatureId, "signatureId");
|
|
2666
|
+
return this.client.buildSseUrl(
|
|
2667
|
+
`${this.basePath}/${budgetId}/signatures/${signatureId}/certificate.pdf`
|
|
2668
|
+
);
|
|
2669
|
+
}
|
|
2670
|
+
// ==========================================
|
|
2671
|
+
// Magic links
|
|
2672
|
+
// ==========================================
|
|
2673
|
+
/**
|
|
2674
|
+
* Generar un magic link de firma. El endpoint es idempotente para la tupla
|
|
2675
|
+
* (budgetId, signerEmail): si ya existe un token vivo se devuelve con
|
|
2676
|
+
* `reused: true` y `signingUrl: null` (el raw token original no se
|
|
2677
|
+
* recupera).
|
|
2678
|
+
*/
|
|
2679
|
+
async generateSigningLink(budgetId, input) {
|
|
2680
|
+
validateRequired(budgetId, "budgetId");
|
|
2681
|
+
validateRequired(input.signerEmail, "signerEmail");
|
|
2682
|
+
return this.client.post(
|
|
2683
|
+
`${this.basePath}/${budgetId}/signing-link`,
|
|
2684
|
+
input
|
|
2685
|
+
);
|
|
2686
|
+
}
|
|
2687
|
+
/**
|
|
2688
|
+
* Listar los magic links vivos del presupuesto (no usados, no revocados y no
|
|
2689
|
+
* expirados). Los filtros `includeRevoked`/`includeExpired` se reservan para
|
|
2690
|
+
* cuando el backend exponga la vista extendida.
|
|
2691
|
+
*/
|
|
2692
|
+
async listSigningLinks(budgetId, filter) {
|
|
2693
|
+
validateRequired(budgetId, "budgetId");
|
|
2694
|
+
const params = this.serializeSigningLinksFilter(filter);
|
|
2695
|
+
const raw = await this.client.get(
|
|
2696
|
+
`${this.basePath}/${budgetId}/signing-links`,
|
|
2697
|
+
{ params }
|
|
2698
|
+
);
|
|
2699
|
+
return raw.items ?? [];
|
|
2700
|
+
}
|
|
2701
|
+
/** Revocar un magic link. Idempotente: llamarla dos veces no falla. */
|
|
2702
|
+
async revokeSigningLink(budgetId, tokenId) {
|
|
2703
|
+
validateRequired(budgetId, "budgetId");
|
|
2704
|
+
validateRequired(tokenId, "tokenId");
|
|
2705
|
+
return this.client.delete(
|
|
2706
|
+
`${this.basePath}/${budgetId}/signing-links/${tokenId}`
|
|
2707
|
+
);
|
|
2708
|
+
}
|
|
2709
|
+
/**
|
|
2710
|
+
* Audit trail paginado por cursor de los eventos de firma del presupuesto
|
|
2711
|
+
* (emisión, visualización, descarga de PDF, aprobación, rechazo,
|
|
2712
|
+
* expiración, revocación, fallos).
|
|
2713
|
+
*/
|
|
2714
|
+
async getAuditTrail(budgetId, filter) {
|
|
2715
|
+
validateRequired(budgetId, "budgetId");
|
|
2716
|
+
const params = this.serializeAuditFilter(filter);
|
|
2717
|
+
const raw = await this.client.get(
|
|
2718
|
+
`${this.basePath}/${budgetId}/signing-links/audit`,
|
|
2719
|
+
{ params }
|
|
2720
|
+
);
|
|
2721
|
+
const items = raw.items ?? [];
|
|
2722
|
+
if (filter?.tokenId) {
|
|
2723
|
+
return items.filter((e) => e.tokenId === filter.tokenId);
|
|
2724
|
+
}
|
|
2725
|
+
return items;
|
|
2726
|
+
}
|
|
2727
|
+
// ==========================================
|
|
2728
|
+
// Helpers privados
|
|
2729
|
+
// ==========================================
|
|
2730
|
+
serializeSigningLinksFilter(filter) {
|
|
2731
|
+
if (!filter) return void 0;
|
|
2732
|
+
const out = {};
|
|
2733
|
+
if (typeof filter.includeRevoked === "boolean") {
|
|
2734
|
+
out.includeRevoked = filter.includeRevoked;
|
|
2735
|
+
}
|
|
2736
|
+
if (typeof filter.includeExpired === "boolean") {
|
|
2737
|
+
out.includeExpired = filter.includeExpired;
|
|
2738
|
+
}
|
|
2739
|
+
if (filter.cursor) out.cursor = filter.cursor;
|
|
2740
|
+
if (typeof filter.limit === "number") out.limit = filter.limit;
|
|
2741
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
2742
|
+
}
|
|
2743
|
+
serializeAuditFilter(filter) {
|
|
2744
|
+
if (!filter) return void 0;
|
|
2745
|
+
const out = {};
|
|
2746
|
+
if (filter.cursor) out.cursor = filter.cursor;
|
|
2747
|
+
if (typeof filter.limit === "number") out.limit = filter.limit;
|
|
2748
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
2749
|
+
}
|
|
2750
|
+
};
|
|
2751
|
+
|
|
2752
|
+
// src/services/organization-settings.service.ts
|
|
2753
|
+
var OrganizationSettingsService = class {
|
|
2754
|
+
constructor(client) {
|
|
2755
|
+
this.basePath = "/organization-settings";
|
|
2756
|
+
this.client = client;
|
|
2757
|
+
}
|
|
2758
|
+
/**
|
|
2759
|
+
* Obtener los datos públicos de la organización.
|
|
2760
|
+
*
|
|
2761
|
+
* Incluye identidad corporativa, branding, configuración de facturación
|
|
2762
|
+
* (moneda, impuesto por defecto, país) y canales de contacto.
|
|
2763
|
+
*
|
|
2764
|
+
* @returns Configuración pública de la organización.
|
|
2765
|
+
*
|
|
2766
|
+
* @example
|
|
2767
|
+
* ```typescript
|
|
2768
|
+
* const settings = await sdk.organizationSettings.getPublicSettings();
|
|
2769
|
+
* console.log(settings.defaultCurrency); // "EUR"
|
|
2770
|
+
* console.log(settings.defaultTaxName); // "IVA"
|
|
2771
|
+
* console.log(settings.defaultTaxRate); // 21
|
|
2772
|
+
* console.log(settings.country); // "ES"
|
|
2773
|
+
* ```
|
|
2774
|
+
*/
|
|
2775
|
+
async getPublicSettings() {
|
|
2776
|
+
return this.client.get(`${this.basePath}/public`);
|
|
2777
|
+
}
|
|
2778
|
+
};
|
|
2779
|
+
|
|
2780
|
+
// src/types/budget.types.ts
|
|
2781
|
+
var BUDGET_WEBHOOK_EVENTS = [
|
|
2782
|
+
"BUDGET_CREATED",
|
|
2783
|
+
"BUDGET_UPDATED",
|
|
2784
|
+
"BUDGET_SENT",
|
|
2785
|
+
"BUDGET_APPROVED",
|
|
2786
|
+
"BUDGET_REJECTED",
|
|
2787
|
+
"BUDGET_CANCELLED",
|
|
2788
|
+
"BUDGET_EXPIRED",
|
|
2789
|
+
"BUDGET_DUPLICATED",
|
|
2790
|
+
"BUDGET_CONVERTED_TO_PROJECT",
|
|
2791
|
+
"BUDGET_CONVERTED_TO_INVOICE"
|
|
2792
|
+
];
|
|
2793
|
+
|
|
1715
2794
|
// src/index.ts
|
|
1716
2795
|
var UtiliaSDK = class {
|
|
1717
2796
|
/**
|
|
@@ -1751,6 +2830,11 @@ var UtiliaSDK = class {
|
|
|
1751
2830
|
this.tickets = new TicketsService(this._client);
|
|
1752
2831
|
this.users = new UsersService(this._client);
|
|
1753
2832
|
this.ai = new AiService(this._client);
|
|
2833
|
+
this.budgets = new BudgetsService(this._client);
|
|
2834
|
+
this.budgetTemplates = new BudgetTemplatesService(this._client);
|
|
2835
|
+
this.budgetComments = new BudgetCommentsService(this._client);
|
|
2836
|
+
this.budgetSignatures = new BudgetSignaturesService(this._client);
|
|
2837
|
+
this.organizationSettings = new OrganizationSettingsService(this._client);
|
|
1754
2838
|
}
|
|
1755
2839
|
/**
|
|
1756
2840
|
* Servicio OAuth (solo disponible si se configuro oauth en el constructor)
|
|
@@ -1761,6 +2845,7 @@ var UtiliaSDK = class {
|
|
|
1761
2845
|
};
|
|
1762
2846
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1763
2847
|
0 && (module.exports = {
|
|
2848
|
+
BUDGET_WEBHOOK_EVENTS,
|
|
1764
2849
|
ErrorCode,
|
|
1765
2850
|
FileTokenStorage,
|
|
1766
2851
|
MemoryTokenStorage,
|