@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/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
- * @param options - Opciones adicionales (state personalizado, scopes)
256
- * @returns URL de autorización, code_verifier y state
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
- const current = await this.storage.getTokens();
416
- if (!current?.refreshToken) {
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
- const response = await this.http.post("/oauth/token", body);
428
- const data = response.data;
429
- const tokens = {
430
- accessToken: data.access_token,
431
- refreshToken: data.refresh_token ?? current.refreshToken,
432
- expiresIn: data.expires_in,
433
- expiresAt: Date.now() + data.expires_in * 1e3,
434
- tokenType: data.token_type || "Bearer",
435
- scope: data.scope,
436
- idToken: data.id_token
437
- };
438
- await this.storage.setTokens(tokens);
439
- return tokens;
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 ('access_token' | 'refresh_token').
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 (tokenType) {
469
- body.token_type_hint = tokenType;
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
- try {
475
- await this.http.post("/oauth/revoke", body);
476
- } catch {
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
- await this.storage.clearTokens();
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,