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