@nubiia/mcp-freematica 0.6.1

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.
Files changed (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +485 -0
  3. package/dist/clients/base-client.d.ts +31 -0
  4. package/dist/clients/base-client.js +102 -0
  5. package/dist/clients/base-client.js.map +1 -0
  6. package/dist/clients/fiql-builder.d.ts +118 -0
  7. package/dist/clients/fiql-builder.js +257 -0
  8. package/dist/clients/fiql-builder.js.map +1 -0
  9. package/dist/clients/freematica-client.d.ts +659 -0
  10. package/dist/clients/freematica-client.js +1327 -0
  11. package/dist/clients/freematica-client.js.map +1 -0
  12. package/dist/clients/hardened-base-client.d.ts +143 -0
  13. package/dist/clients/hardened-base-client.js +366 -0
  14. package/dist/clients/hardened-base-client.js.map +1 -0
  15. package/dist/config.d.ts +104 -0
  16. package/dist/config.js +105 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/index.d.ts +2 -0
  19. package/dist/index.js +55 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/logger.d.ts +71 -0
  22. package/dist/logger.js +114 -0
  23. package/dist/logger.js.map +1 -0
  24. package/dist/schemas/albaranes.d.ts +124 -0
  25. package/dist/schemas/albaranes.js +250 -0
  26. package/dist/schemas/albaranes.js.map +1 -0
  27. package/dist/schemas/cartera.d.ts +70 -0
  28. package/dist/schemas/cartera.js +104 -0
  29. package/dist/schemas/cartera.js.map +1 -0
  30. package/dist/schemas/contratos.d.ts +7 -0
  31. package/dist/schemas/contratos.js +2 -0
  32. package/dist/schemas/contratos.js.map +1 -0
  33. package/dist/schemas/facturas-electronicas.d.ts +60 -0
  34. package/dist/schemas/facturas-electronicas.js +79 -0
  35. package/dist/schemas/facturas-electronicas.js.map +1 -0
  36. package/dist/schemas/facturas-ventas.d.ts +118 -0
  37. package/dist/schemas/facturas-ventas.js +153 -0
  38. package/dist/schemas/facturas-ventas.js.map +1 -0
  39. package/dist/schemas/filters.d.ts +92 -0
  40. package/dist/schemas/filters.js +76 -0
  41. package/dist/schemas/filters.js.map +1 -0
  42. package/dist/schemas/master-data.d.ts +26 -0
  43. package/dist/schemas/master-data.js +86 -0
  44. package/dist/schemas/master-data.js.map +1 -0
  45. package/dist/schemas/pagination.d.ts +14 -0
  46. package/dist/schemas/pagination.js +27 -0
  47. package/dist/schemas/pagination.js.map +1 -0
  48. package/dist/schemas/pedidos-compras.d.ts +92 -0
  49. package/dist/schemas/pedidos-compras.js +189 -0
  50. package/dist/schemas/pedidos-compras.js.map +1 -0
  51. package/dist/server-instructions.d.ts +1 -0
  52. package/dist/server-instructions.js +83 -0
  53. package/dist/server-instructions.js.map +1 -0
  54. package/dist/server.d.ts +6 -0
  55. package/dist/server.js +41 -0
  56. package/dist/server.js.map +1 -0
  57. package/dist/tools/albaranes.d.ts +16 -0
  58. package/dist/tools/albaranes.js +202 -0
  59. package/dist/tools/albaranes.js.map +1 -0
  60. package/dist/tools/calendarios.d.ts +13 -0
  61. package/dist/tools/calendarios.js +82 -0
  62. package/dist/tools/calendarios.js.map +1 -0
  63. package/dist/tools/cartera.d.ts +13 -0
  64. package/dist/tools/cartera.js +78 -0
  65. package/dist/tools/cartera.js.map +1 -0
  66. package/dist/tools/clientes.d.ts +3 -0
  67. package/dist/tools/clientes.js +48 -0
  68. package/dist/tools/clientes.js.map +1 -0
  69. package/dist/tools/contabilidad.d.ts +14 -0
  70. package/dist/tools/contabilidad.js +259 -0
  71. package/dist/tools/contabilidad.js.map +1 -0
  72. package/dist/tools/contactos-clientes.d.ts +3 -0
  73. package/dist/tools/contactos-clientes.js +25 -0
  74. package/dist/tools/contactos-clientes.js.map +1 -0
  75. package/dist/tools/contratos.d.ts +10 -0
  76. package/dist/tools/contratos.js +35 -0
  77. package/dist/tools/contratos.js.map +1 -0
  78. package/dist/tools/facturas-compras.d.ts +13 -0
  79. package/dist/tools/facturas-compras.js +131 -0
  80. package/dist/tools/facturas-compras.js.map +1 -0
  81. package/dist/tools/facturas-electronicas.d.ts +17 -0
  82. package/dist/tools/facturas-electronicas.js +268 -0
  83. package/dist/tools/facturas-electronicas.js.map +1 -0
  84. package/dist/tools/facturas-ventas.d.ts +16 -0
  85. package/dist/tools/facturas-ventas.js +181 -0
  86. package/dist/tools/facturas-ventas.js.map +1 -0
  87. package/dist/tools/helpers.d.ts +17 -0
  88. package/dist/tools/helpers.js +36 -0
  89. package/dist/tools/helpers.js.map +1 -0
  90. package/dist/tools/localizaciones.d.ts +17 -0
  91. package/dist/tools/localizaciones.js +192 -0
  92. package/dist/tools/localizaciones.js.map +1 -0
  93. package/dist/tools/master-data.d.ts +10 -0
  94. package/dist/tools/master-data.js +70 -0
  95. package/dist/tools/master-data.js.map +1 -0
  96. package/dist/tools/oportunidades-negocio.d.ts +3 -0
  97. package/dist/tools/oportunidades-negocio.js +68 -0
  98. package/dist/tools/oportunidades-negocio.js.map +1 -0
  99. package/dist/tools/pedidos-compras.d.ts +14 -0
  100. package/dist/tools/pedidos-compras.js +105 -0
  101. package/dist/tools/pedidos-compras.js.map +1 -0
  102. package/dist/tools/personal.d.ts +13 -0
  103. package/dist/tools/personal.js +154 -0
  104. package/dist/tools/personal.js.map +1 -0
  105. package/dist/tools/prl.d.ts +47 -0
  106. package/dist/tools/prl.js +242 -0
  107. package/dist/tools/prl.js.map +1 -0
  108. package/dist/tools/proveedores.d.ts +14 -0
  109. package/dist/tools/proveedores.js +122 -0
  110. package/dist/tools/proveedores.js.map +1 -0
  111. package/dist/transports/http.d.ts +12 -0
  112. package/dist/transports/http.js +110 -0
  113. package/dist/transports/http.js.map +1 -0
  114. package/dist/transports/stdio.d.ts +12 -0
  115. package/dist/transports/stdio.js +15 -0
  116. package/dist/transports/stdio.js.map +1 -0
  117. package/dist/types/api-envelope.d.ts +27 -0
  118. package/dist/types/api-envelope.js +2 -0
  119. package/dist/types/api-envelope.js.map +1 -0
  120. package/dist/types/clientes.d.ts +9 -0
  121. package/dist/types/clientes.js +2 -0
  122. package/dist/types/clientes.js.map +1 -0
  123. package/dist/types/contactos-clientes.d.ts +7 -0
  124. package/dist/types/contactos-clientes.js +2 -0
  125. package/dist/types/contactos-clientes.js.map +1 -0
  126. package/dist/types/contratos.d.ts +8 -0
  127. package/dist/types/contratos.js +2 -0
  128. package/dist/types/contratos.js.map +1 -0
  129. package/dist/types/master-data.d.ts +9 -0
  130. package/dist/types/master-data.js +2 -0
  131. package/dist/types/master-data.js.map +1 -0
  132. package/dist/types/oportunidades-negocio.d.ts +8 -0
  133. package/dist/types/oportunidades-negocio.js +2 -0
  134. package/dist/types/oportunidades-negocio.js.map +1 -0
  135. package/dist/utils/size-guardrail.d.ts +22 -0
  136. package/dist/utils/size-guardrail.js +31 -0
  137. package/dist/utils/size-guardrail.js.map +1 -0
  138. package/package.json +90 -0
@@ -0,0 +1,1327 @@
1
+ import { BaseClient } from './base-client.js';
2
+ import { buildFiql, appendRquery } from './fiql-builder.js';
3
+ import { CATALOG_ENDPOINTS } from '../schemas/master-data.js';
4
+ import { ESTADO_CARTERA_FIQL_MAP, } from '../schemas/cartera.js';
5
+ import { buildEstadoPedidoFiql, } from '../schemas/pedidos-compras.js';
6
+ import { logger } from '../logger.js';
7
+ import { loadMaxResponseSizeMb } from '../utils/size-guardrail.js';
8
+ /**
9
+ * Typed client for the Freemática REST API.
10
+ *
11
+ * One method per exposed endpoint (or per family of endpoints). The wrapper
12
+ * `{ errorCode, errorMessage, data }` is unwrapped by `BaseClient.request`,
13
+ * so methods only see `data`.
14
+ *
15
+ * For list endpoints, `data` has shape `{ total, items, rowHeight }`. List
16
+ * methods unwrap that to `{ items, total }` (string total → number; rowHeight
17
+ * discarded).
18
+ *
19
+ * For detail endpoints, `data` is the entity directly.
20
+ */
21
+ export class FreematicaClient extends BaseClient {
22
+ // ---------------------------------------------------------------------------
23
+ // Existing endpoints (v0.1.0, v0.3.0) — shape adjusted for the unwrap fix
24
+ // ---------------------------------------------------------------------------
25
+ /**
26
+ * Obtener lista de material asignado a servicios.
27
+ *
28
+ * Endpoint: GET /pvss/v2/contratos-servicios-material
29
+ */
30
+ async getMaterialesAsignadosServicios() {
31
+ const data = await this.get('/pvss/v2/contratos-servicios-material');
32
+ return { items: data.items, total: Number(data.total) };
33
+ }
34
+ /**
35
+ * Obtener un catálogo de datos maestros.
36
+ */
37
+ async getMasterData(catalog) {
38
+ const endpoint = CATALOG_ENDPOINTS[catalog];
39
+ const data = await this.get(endpoint);
40
+ return { items: data.items, total: Number(data.total) };
41
+ }
42
+ // ---------------------------------------------------------------------------
43
+ // Clientes (v0.4.0)
44
+ // ---------------------------------------------------------------------------
45
+ /** Lista paginada de clientes. */
46
+ async listClientes(opts = {}) {
47
+ return this.listResource('/pgrl/v2/clientes', opts);
48
+ }
49
+ /** Detalle de un cliente por `idReg` opaco. */
50
+ async getCliente(idReg) {
51
+ return this.get(`/pgrl/v2/clientes/${encodeURIComponent(idReg)}`);
52
+ }
53
+ // ---------------------------------------------------------------------------
54
+ // Contactos clientes (v0.4.0)
55
+ // ---------------------------------------------------------------------------
56
+ /** Lista paginada de contactos de clientes. */
57
+ async listContactosClientes(opts = {}) {
58
+ return this.listResource('/pgrl/v2/contactos-clientes', opts);
59
+ }
60
+ // ---------------------------------------------------------------------------
61
+ // Oportunidades de negocio (v0.4.0)
62
+ // ---------------------------------------------------------------------------
63
+ /** Lista paginada de oportunidades de negocio. */
64
+ async listOportunidadesNegocio(opts = {}) {
65
+ return this.listResource('/pcrm/v2/oportunidades-negocio', opts);
66
+ }
67
+ /** Detalle de una oportunidad por `idReg` opaco. */
68
+ async getOportunidadNegocio(idReg) {
69
+ return this.get(`/pcrm/v2/oportunidades-negocio/${encodeURIComponent(idReg)}`);
70
+ }
71
+ /**
72
+ * Datos ampliados de una oportunidad por `idReg` opaco.
73
+ *
74
+ * Puede devolver `not_found` si la oportunidad no tiene datos ampliados.
75
+ * El caller debe manejar el `FreematicaError` con código `not_found`.
76
+ */
77
+ async getOportunidadNegocioDatosAmpliados(idReg) {
78
+ return this.get(`/pcrm/v2/oportunidades-negocio/${encodeURIComponent(idReg)}/datos-ampliados`);
79
+ }
80
+ // ---------------------------------------------------------------------------
81
+ // PRL — Prevención de Riesgos Laborales (v0.5.0)
82
+ // ---------------------------------------------------------------------------
83
+ /**
84
+ * Opciones de filtro para getFichaPrevCliente.
85
+ *
86
+ * Los query params mapean directamente a los parámetros nativos del endpoint
87
+ * GET /pprl/v2/ficha-prev-cliente. NO se usa FIQL en este endpoint.
88
+ */
89
+ /**
90
+ * Obtiene la ficha de PRL de un cliente.
91
+ *
92
+ * Endpoint: GET /pprl/v2/ficha-prev-cliente
93
+ *
94
+ * Los filtros se envían como query params nativos (codCli, grpCli, locServ,
95
+ * codigo). La validación de "al menos uno" es responsabilidad del caller
96
+ * (la tool lo hace via Zod .refine()).
97
+ *
98
+ * @param opts - Filtros: codCliente, grupoCliente, codLocalizacionServicio, codigoFicha.
99
+ * @returns Lista de fichas PRL que coinciden con los filtros.
100
+ */
101
+ async getFichaPrevCliente(opts) {
102
+ const url = new URL('placeholder://x/pprl/v2/ficha-prev-cliente');
103
+ if (opts.codCliente !== undefined)
104
+ url.searchParams.set('codCli', opts.codCliente);
105
+ if (opts.grupoCliente !== undefined)
106
+ url.searchParams.set('grpCli', String(opts.grupoCliente));
107
+ if (opts.codLocalizacionServicio !== undefined)
108
+ url.searchParams.set('locServ', String(opts.codLocalizacionServicio));
109
+ if (opts.codigoFicha !== undefined)
110
+ url.searchParams.set('codigo', opts.codigoFicha);
111
+ const qs = url.searchParams.toString();
112
+ const path = qs ? `/pprl/v2/ficha-prev-cliente?${qs}` : '/pprl/v2/ficha-prev-cliente';
113
+ const data = await this.get(path);
114
+ return { items: data.items, total: Number(data.total) };
115
+ }
116
+ // ---------------------------------------------------------------------------
117
+ // Contabilidad — cuentas contables (v0.5.0)
118
+ // ---------------------------------------------------------------------------
119
+ /**
120
+ * Lista de cuentas del plan contable.
121
+ *
122
+ * Endpoint: GET /pcon/v2/cuentas
123
+ *
124
+ * Los filtros opcionales se mapean a una expresión FIQL y se pasan como
125
+ * query param `rquery`. El prefijoCuenta usa operador `=ge=` / `=lt=` para
126
+ * emular un prefix match sobre COD_CTA (rango léxico).
127
+ *
128
+ * @param opts - Filtros opcionales: codPlan, prefijoCuenta, activa, grupoCuenta.
129
+ */
130
+ async listCuentasContables(opts = {}) {
131
+ const url = new URL('https://placeholder/pcon/v2/cuentas');
132
+ const filters = {};
133
+ if (opts.codPlan !== undefined)
134
+ filters['COD_PLAN'] = opts.codPlan;
135
+ if (opts.activa !== undefined)
136
+ filters['CTA_ACTIVA'] = opts.activa ? '1' : '0';
137
+ if (opts.grupoCuenta !== undefined)
138
+ filters['COD_GRUPO_CTA'] = opts.grupoCuenta;
139
+ const andGroups = [];
140
+ if (Object.keys(filters).length > 0) {
141
+ andGroups.push(filters);
142
+ }
143
+ // Prefix match sobre COD_CTA: COD_CTA >= prefix AND COD_CTA < prefix_next
144
+ if (opts.prefijoCuenta !== undefined) {
145
+ const prefix = opts.prefijoCuenta;
146
+ const nextPrefix = buildNextPrefix(prefix);
147
+ andGroups.push({ COD_CTA: { op: 'ge', value: prefix } });
148
+ if (nextPrefix !== null) {
149
+ andGroups.push({ COD_CTA: { op: 'lt', value: nextPrefix } });
150
+ }
151
+ }
152
+ const fiql = andGroups.length > 0 ? buildFiql({ and: andGroups }) : '';
153
+ appendRquery(url, fiql);
154
+ const path = url.pathname + (url.search ? url.search : '');
155
+ const data = await this.get(path);
156
+ return { items: data.items, total: Number(data.total) };
157
+ }
158
+ /**
159
+ * Opciones de filtro para listVigilanciaSalud.
160
+ */
161
+ /**
162
+ * Lista paginada de registros de Vigilancia de la Salud.
163
+ *
164
+ * Endpoint: GET /pprl/v1/vigilancia-salud
165
+ *
166
+ * `idRegPersona` se envía como query param nativo. El resto de filtros se
167
+ * codifican en FIQL y se envían como `rquery`.
168
+ *
169
+ * @param opts - Opciones de paginación y filtrado.
170
+ * @returns Lista paginada de registros VS.
171
+ */
172
+ async listVigilanciaSalud(opts) {
173
+ const url = new URL('placeholder://x/pprl/v1/vigilancia-salud');
174
+ if (opts.items !== undefined)
175
+ url.searchParams.set('items', String(opts.items));
176
+ if (opts.page !== undefined)
177
+ url.searchParams.set('page', String(opts.page));
178
+ if (opts.idRegPersona !== undefined)
179
+ url.searchParams.set('idRegPersona', opts.idRegPersona);
180
+ // Filtros FIQL
181
+ const fiqlParts = {};
182
+ if (opts.empresa !== undefined)
183
+ fiqlParts['PERVS_EMP'] = opts.empresa;
184
+ if (opts.delegacion !== undefined)
185
+ fiqlParts['PERVS_DELEG'] = opts.delegacion;
186
+ if (opts.codPersona !== undefined)
187
+ fiqlParts['PERVS_PERSO'] = opts.codPersona;
188
+ if (opts.tipoRevision !== undefined)
189
+ fiqlParts['PERVS_TIPO_REVISION'] = opts.tipoRevision;
190
+ if (opts.resultado !== undefined)
191
+ fiqlParts['PERVS_RESULTADO'] = opts.resultado;
192
+ if (opts.fechaCitaDesde !== undefined)
193
+ fiqlParts['PERVS_FCH_CITA'] = { op: 'ge', value: opts.fechaCitaDesde };
194
+ // If both desde and hasta, we need to combine with AND
195
+ const fiqlParts2 = {};
196
+ if (opts.fechaCitaHasta !== undefined)
197
+ fiqlParts2['PERVS_FCH_CITA'] = { op: 'le', value: opts.fechaCitaHasta };
198
+ const fiql1 = buildFiql(fiqlParts);
199
+ const fiql2 = buildFiql(fiqlParts2);
200
+ const combinedFiql = [fiql1, fiql2].filter(Boolean).join(';');
201
+ appendRquery(url, combinedFiql);
202
+ const qs = url.searchParams.toString();
203
+ const path = qs ? `/pprl/v1/vigilancia-salud?${qs}` : '/pprl/v1/vigilancia-salud';
204
+ const data = await this.get(path);
205
+ return { items: data.items, total: Number(data.total) };
206
+ }
207
+ // ---------------------------------------------------------------------------
208
+ // Contabilidad — cuentas analíticas (v0.5.0)
209
+ // ---------------------------------------------------------------------------
210
+ /**
211
+ * Lista de cuentas analíticas.
212
+ *
213
+ * Endpoint: GET /pcon/v2/cuentas-analiticas
214
+ *
215
+ * @param opts - Filtros opcionales: codPlan, prefijoCuenta, activa, grupoCuenta, area, delegacion.
216
+ */
217
+ async listCuentasAnaliticas(opts = {}) {
218
+ const url = new URL('https://placeholder/pcon/v2/cuentas-analiticas');
219
+ const filters = {};
220
+ if (opts.codPlan !== undefined)
221
+ filters['COD_PLAN'] = opts.codPlan;
222
+ if (opts.activa !== undefined)
223
+ filters['CTA_ACTIVA_ANL'] = opts.activa ? '1' : '0';
224
+ if (opts.grupoCuenta !== undefined)
225
+ filters['COD_GRUPO_ANL'] = opts.grupoCuenta;
226
+ if (opts.area !== undefined)
227
+ filters['AREA_ANL'] = opts.area;
228
+ if (opts.delegacion !== undefined)
229
+ filters['DELEG'] = opts.delegacion;
230
+ const andGroups = [];
231
+ if (Object.keys(filters).length > 0) {
232
+ andGroups.push(filters);
233
+ }
234
+ // Prefix match sobre COD_CTA_ANL
235
+ if (opts.prefijoCuenta !== undefined) {
236
+ const prefix = opts.prefijoCuenta;
237
+ const nextPrefix = buildNextPrefix(prefix);
238
+ andGroups.push({ COD_CTA_ANL: { op: 'ge', value: prefix } });
239
+ if (nextPrefix !== null) {
240
+ andGroups.push({ COD_CTA_ANL: { op: 'lt', value: nextPrefix } });
241
+ }
242
+ }
243
+ const fiql = andGroups.length > 0 ? buildFiql({ and: andGroups }) : '';
244
+ appendRquery(url, fiql);
245
+ const path = url.pathname + (url.search ? url.search : '');
246
+ const data = await this.get(path);
247
+ return { items: data.items, total: Number(data.total) };
248
+ }
249
+ /**
250
+ * Detalle de un registro de Vigilancia de la Salud por `idReg` opaco.
251
+ *
252
+ * Endpoint: GET /pprl/v1/vigilancia-salud/{idreg}
253
+ *
254
+ * @param idReg - Identificador opaco del registro.
255
+ * @returns Objeto con todos los campos del registro VS.
256
+ */
257
+ async getVigilanciaSalud(idReg) {
258
+ return this.get(`/pprl/v1/vigilancia-salud/${encodeURIComponent(idReg)}`);
259
+ }
260
+ // ---------------------------------------------------------------------------
261
+ // Personal — RRHH (v0.5.0)
262
+ // ---------------------------------------------------------------------------
263
+ /**
264
+ * Lista paginada de personas (empleados) con filtros FIQL opcionales.
265
+ *
266
+ * Endpoint: GET /pers/v2/personal
267
+ *
268
+ * El filtro `activo` (boolean) se traduce a `VSSPER_ACTIVO==S` (true) o
269
+ * `VSSPER_ACTIVO==N` (false). Las búsquedas por `nombre` y `apellido` usan
270
+ * el operador de igualdad estándar FIQL (el servidor aplica LIKE internamente
271
+ * para estos campos).
272
+ *
273
+ * @param opts - Opciones de paginación y filtrado.
274
+ * @returns Lista paginada de personas.
275
+ */
276
+ async listPersonal(opts) {
277
+ const url = new URL('placeholder://x/pers/v2/personal');
278
+ if (opts.items !== undefined)
279
+ url.searchParams.set('items', String(opts.items));
280
+ if (opts.page !== undefined)
281
+ url.searchParams.set('page', String(opts.page));
282
+ const fiqlGroup = {};
283
+ if (opts.empresa !== undefined)
284
+ fiqlGroup['VSSPER_EMP'] = opts.empresa;
285
+ if (opts.delegacion !== undefined)
286
+ fiqlGroup['VSSPER_DELEG'] = opts.delegacion;
287
+ if (opts.codPersona !== undefined)
288
+ fiqlGroup['VSSPER_COD'] = opts.codPersona;
289
+ if (opts.nombre !== undefined)
290
+ fiqlGroup['VSSPER_NOM'] = opts.nombre;
291
+ if (opts.apellido !== undefined)
292
+ fiqlGroup['VSSPER_APELL1'] = opts.apellido;
293
+ if (opts.nif !== undefined)
294
+ fiqlGroup['VSSPER_NIF'] = opts.nif;
295
+ if (opts.situacion !== undefined)
296
+ fiqlGroup['VSSPER_SIT'] = opts.situacion;
297
+ if (opts.departamento !== undefined)
298
+ fiqlGroup['VSSPER_DPTO'] = opts.departamento;
299
+ if (opts.seccion !== undefined)
300
+ fiqlGroup['VSSPER_SECCION'] = opts.seccion;
301
+ if (opts.activo !== undefined)
302
+ fiqlGroup['VSSPER_ACTIVO'] = opts.activo ? 'S' : 'N';
303
+ appendRquery(url, buildFiql(fiqlGroup));
304
+ const qs = url.searchParams.toString();
305
+ const path = qs ? `/pers/v2/personal?${qs}` : '/pers/v2/personal';
306
+ const data = await this.get(path);
307
+ return { items: data.items, total: Number(data.total) };
308
+ }
309
+ /**
310
+ * Detalle de una persona por `idReg` opaco.
311
+ *
312
+ * Endpoint: GET /pers/v2/personal/{idreg}
313
+ *
314
+ * @param idReg - Identificador opaco de la persona.
315
+ * @returns Objeto con todos los campos de la persona.
316
+ */
317
+ async getPersona(idReg) {
318
+ return this.get(`/pers/v2/personal/${encodeURIComponent(idReg)}`);
319
+ }
320
+ // ---------------------------------------------------------------------------
321
+ // Calendarios laborales (v0.5.0)
322
+ // ---------------------------------------------------------------------------
323
+ /**
324
+ * Lista paginada de calendarios laborales.
325
+ *
326
+ * Endpoint: GET /pgrl/v1/calendarios
327
+ *
328
+ * @param opts - Opciones de paginación.
329
+ * @returns Lista paginada de calendarios.
330
+ */
331
+ async listCalendarios(opts = {}) {
332
+ return this.listResource('/pgrl/v1/calendarios', opts);
333
+ }
334
+ /**
335
+ * Lista paginada de periodos de un calendario laboral.
336
+ *
337
+ * Endpoint: GET /pgrl/v1/calendarios/{idreg}/periodos
338
+ *
339
+ * @param idCalendario - idReg opaco del calendario.
340
+ * @param opts - Opciones de paginación.
341
+ * @returns Lista paginada de periodos del calendario.
342
+ */
343
+ async listCalendarioPeriodos(idCalendario, opts = {}) {
344
+ return this.listResource(`/pgrl/v1/calendarios/${encodeURIComponent(idCalendario)}/periodos`, opts);
345
+ }
346
+ // ---------------------------------------------------------------------------
347
+ // Contabilidad — export asientos (v0.5.0)
348
+ // ---------------------------------------------------------------------------
349
+ /**
350
+ * Exporta asientos contables de Freemática.
351
+ *
352
+ * Endpoint: GET /pcon/v2/export-asientos
353
+ *
354
+ * Los parámetros `empresa`, `cal` y `periodo` son query params nativos.
355
+ * Los filtros `fechaDesde`, `fechaHasta`, `diario` y `borrador` se envían
356
+ * como expresión FIQL en el param `rquery`.
357
+ *
358
+ * Protección de tamaño: si el JSON serializado supera
359
+ * `FREEMATICA_MAX_RESPONSE_SIZE_MB` (default 10 MB), los items se truncan
360
+ * y se añade un campo `warning` a la respuesta.
361
+ *
362
+ * @param opts - Opciones de exportación.
363
+ * @returns Resultado con `items`, `total`, y opcionalmente `truncated`+`warning`.
364
+ */
365
+ async exportAsientos(opts) {
366
+ const maxMb = loadMaxResponseSizeMb();
367
+ const maxBytes = maxMb * 1024 * 1024;
368
+ // Construir URL con params nativos
369
+ const url = new URL('https://placeholder/pcon/v2/export-asientos');
370
+ url.searchParams.set('empresa', opts.empresa);
371
+ url.searchParams.set('cal', opts.cal);
372
+ if (opts.periodo !== undefined)
373
+ url.searchParams.set('periodo', opts.periodo);
374
+ // Construir FIQL para filtros adicionales
375
+ const andGroups = [];
376
+ if (opts.fechaDesde !== undefined) {
377
+ andGroups.push({ ASI_FCHASI: { op: 'ge', value: opts.fechaDesde } });
378
+ }
379
+ if (opts.fechaHasta !== undefined) {
380
+ andGroups.push({ ASI_FCHASI: { op: 'le', value: opts.fechaHasta } });
381
+ }
382
+ if (opts.diario !== undefined) {
383
+ andGroups.push({ ASI_DIARIO: opts.diario });
384
+ }
385
+ if (opts.borrador !== undefined) {
386
+ /**
387
+ * ASUNCIÓN EMPÍRICA — convención para campo nullable en Freemática FIQL
388
+ *
389
+ * La API de Freemática no expone un operador IS NULL nativo en FIQL. La
390
+ * convención observada en otras tablas del sistema es representar el valor
391
+ * nulo/ausente como cadena vacía `''` en las expresiones `=eq=` / `=ne=`.
392
+ *
393
+ * Para `ASI_BORR`:
394
+ * - borrador=true → `ASI_BORR!=''` (campo tiene algún valor → asiento en borrador)
395
+ * - borrador=false → `ASI_BORR==''` (campo vacío / null → asiento definitivo)
396
+ *
397
+ * Esta convención NO ha sido verificada contra la API real de Freemática.
398
+ * Si en pruebas funcionales el filtro no funciona correctamente, puede que
399
+ * el centinela sea el literal `null` en lugar de `''`, o que el campo
400
+ * use un valor boolean/entero como en CTA_ACTIVA.
401
+ *
402
+ * TODO(TD-???): verificar contra API real cuando esté disponible.
403
+ */
404
+ andGroups.push({ ASI_BORR: { op: opts.borrador ? 'ne' : 'eq', value: '' } });
405
+ }
406
+ const fiql = andGroups.length > 0 ? buildFiql({ and: andGroups }) : '';
407
+ appendRquery(url, fiql);
408
+ const path = url.pathname + url.search;
409
+ const data = await this.get(path);
410
+ const items = data.items;
411
+ const total = Number(data.total);
412
+ // Validación de tamaño máximo
413
+ const serialized = JSON.stringify(items);
414
+ const byteSize = Buffer.byteLength(serialized, 'utf8');
415
+ if (byteSize > maxBytes) {
416
+ logger.warn({
417
+ byteSize,
418
+ maxBytes,
419
+ maxMb,
420
+ totalItems: items.length,
421
+ empresa: opts.empresa,
422
+ cal: opts.cal,
423
+ }, 'export-asientos: respuesta excede MAX_RESPONSE_SIZE_MB, truncando');
424
+ // Truncar items hasta que quepan en el límite
425
+ const truncatedItems = truncateItemsToLimit(items, maxBytes);
426
+ return {
427
+ items: truncatedItems,
428
+ total,
429
+ truncated: true,
430
+ warning: [
431
+ `La respuesta ha sido truncada porque superaba el límite de ${maxMb} MB.`,
432
+ `Items devueltos: ${truncatedItems.length} de ${items.length} totales.`,
433
+ `Usa rangos de fecha cortos (fechaDesde + fechaHasta) para obtener datos completos.`,
434
+ ].join(' '),
435
+ };
436
+ }
437
+ return { items, total, truncated: false };
438
+ }
439
+ // ---------------------------------------------------------------------------
440
+ // Facturas de compras (v0.5.0)
441
+ // ---------------------------------------------------------------------------
442
+ /**
443
+ * Lista paginada de facturas de compras con filtros FIQL + query params nativos.
444
+ *
445
+ * El campo `exportado` es un enum nativo del endpoint (no FIQL): `all` | `not_exported`.
446
+ * El resto de filtros se envían mediante el query param `rquery` (FIQL).
447
+ *
448
+ * Endpoint: GET /pcmp/v2/facturas-compras
449
+ */
450
+ async listFacturasCompras(opts = {}) {
451
+ const url = new URL('https://placeholder/pcmp/v2/facturas-compras');
452
+ if (opts.items !== undefined)
453
+ url.searchParams.set('items', String(opts.items));
454
+ if (opts.page !== undefined)
455
+ url.searchParams.set('page', String(opts.page));
456
+ if (opts.exportado !== undefined)
457
+ url.searchParams.set('exportado', opts.exportado);
458
+ // Construir grupos AND para el filtro de fecha (ambos usan FCC_FCHFAC con distinto operador).
459
+ // FCC_FCHFAC_HASTA NO existe en el schema Freemática; no se puede usar como campo sintético.
460
+ // La solución correcta es la composición AND: dos expresiones con el mismo campo real.
461
+ const dateGroups = [];
462
+ if (opts.fechaDesde !== undefined) {
463
+ dateGroups.push({ FCC_FCHFAC: { op: 'ge', value: opts.fechaDesde } });
464
+ }
465
+ if (opts.fechaHasta !== undefined) {
466
+ dateGroups.push({ FCC_FCHFAC: { op: 'le', value: opts.fechaHasta } });
467
+ }
468
+ // Resto de filtros escalares en un grupo plano
469
+ const scalarGroup = {
470
+ FCC_CODEMP: opts.empresa,
471
+ FCC_CODPRO: opts.codProveedor,
472
+ FCC_SERIEFRA: opts.serie,
473
+ FCC_NUMFRA: opts.numFactura,
474
+ FCC_FPAGO: opts.formaPago,
475
+ FCC_TRASP_CONTAB: opts.traspasadoContabilidad !== undefined
476
+ ? String(opts.traspasadoContabilidad)
477
+ : undefined,
478
+ FCC_DELEG: opts.delegacion,
479
+ FCC_LIN_NEGOCIO: opts.lineaNegocio,
480
+ };
481
+ const fiql = buildFiql({ and: [...dateGroups, scalarGroup] });
482
+ appendRquery(url, fiql);
483
+ const path = url.pathname + (url.search ? url.search : '');
484
+ const data = await this.get(path);
485
+ return { items: data.items, total: Number(data.total) };
486
+ }
487
+ // ---------------------------------------------------------------------------
488
+ // Cartera de clientes (v0.5.0)
489
+ // ---------------------------------------------------------------------------
490
+ /**
491
+ * Lista paginada de documentos de cartera de clientes con filtros FIQL.
492
+ *
493
+ * Endpoint: GET /pcar/v1/cartera-clientes
494
+ *
495
+ * Construye la FIQL a partir de los filtros tipados y la añade como `rquery`
496
+ * usando `appendRquery()` (patrón canónico del foundation TD-117).
497
+ *
498
+ * El filtro `soloImpagados=true` genera `CARCL_FECIMPAG!=null`, donde el
499
+ * valor `'null'` es la convención centinela de Freemática para indicar "sin
500
+ * valor" en campos de fecha opcionales (assumption empírica: observado en
501
+ * datos reales de la API; documentado para futuras implementaciones).
502
+ * El operador `ne` se pasa vía `buildFiql` para garantizar escaping
503
+ * consistente con el resto de filtros.
504
+ *
505
+ * El filtro `estado` mapea a los valores numéricos de `CARCL_SITCAR`
506
+ * (1=pendiente, 2=cancelado, 3=derivado).
507
+ *
508
+ * @param opts - Filtros tipados del schema `ListCarteraFiltersSchema`.
509
+ * @returns Lista paginada de documentos de cartera.
510
+ */
511
+ async listCarteraClientes(opts = {}) {
512
+ const url = new URL('/pcar/v1/cartera-clientes', 'http://x');
513
+ if (opts.items !== undefined)
514
+ url.searchParams.set('items', String(opts.items));
515
+ if (opts.page !== undefined)
516
+ url.searchParams.set('page', String(opts.page));
517
+ // Build FIQL parts. Each scalar filter is a separate buildFiql call so that
518
+ // two range constraints on the same field (e.g. CARCL_FECDOC =ge= X and
519
+ // CARCL_FECDOC =le= Y) don't overwrite each other in a single object.
520
+ const parts = [];
521
+ // Equality filters (all go into one group → joined with AND)
522
+ const eqGroup = {};
523
+ if (opts.empresa !== undefined)
524
+ eqGroup['CARCL_EMP'] = opts.empresa;
525
+ if (opts.codCliente !== undefined)
526
+ eqGroup['CARCL_CODAUX'] = opts.codCliente;
527
+ if (opts.grupoCliente !== undefined)
528
+ eqGroup['CARCL_GRUPAUX'] = opts.grupoCliente;
529
+ if (opts.representante !== undefined)
530
+ eqGroup['CARCL_CODREP'] = opts.representante;
531
+ if (opts.formaPago !== undefined)
532
+ eqGroup['CARCL_CODFPAG'] = opts.formaPago;
533
+ if (opts.modoPago !== undefined)
534
+ eqGroup['CARCL_CODMPAG'] = opts.modoPago;
535
+ if (opts.estado !== undefined)
536
+ eqGroup['CARCL_SITCAR'] = ESTADO_CARTERA_FIQL_MAP[opts.estado];
537
+ if (opts.referencia !== undefined)
538
+ eqGroup['CARCL_REFCAR'] = opts.referencia;
539
+ const eqFiql = buildFiql(eqGroup);
540
+ if (eqFiql)
541
+ parts.push(eqFiql);
542
+ // Range filters: each gets its own buildFiql call to preserve both endpoints
543
+ if (opts.fechaDocDesde !== undefined)
544
+ parts.push(buildFiql({ CARCL_FECDOC: { op: 'ge', value: opts.fechaDocDesde } }));
545
+ if (opts.fechaDocHasta !== undefined)
546
+ parts.push(buildFiql({ CARCL_FECDOC: { op: 'le', value: opts.fechaDocHasta } }));
547
+ if (opts.fechaVencimientoDesde !== undefined)
548
+ parts.push(buildFiql({ CARCL_FECVCTO: { op: 'ge', value: opts.fechaVencimientoDesde } }));
549
+ if (opts.fechaVencimientoHasta !== undefined)
550
+ parts.push(buildFiql({ CARCL_FECVCTO: { op: 'le', value: opts.fechaVencimientoHasta } }));
551
+ // soloImpagados: 'null' es el centinela de Freemática para fecha sin valor.
552
+ // Se pasa por buildFiql (op 'ne') igual que el resto de filtros para
553
+ // garantizar escaping uniforme y legibilidad del código.
554
+ if (opts.soloImpagados === true) {
555
+ parts.push(buildFiql({ CARCL_FECIMPAG: { op: 'ne', value: 'null' } }));
556
+ }
557
+ appendRquery(url, parts.join(';'));
558
+ const path = url.pathname + (url.search ? url.search : '');
559
+ const data = await this.get(path);
560
+ return { items: data.items, total: Number(data.total) };
561
+ }
562
+ /**
563
+ * Detalle de una factura de compra por `idReg` opaco.
564
+ *
565
+ * Endpoint: GET /pcmp/v2/facturas-compras/{idReg}
566
+ */
567
+ async getFacturaCompra(idReg) {
568
+ return this.get(`/pcmp/v2/facturas-compras/${encodeURIComponent(idReg)}`);
569
+ }
570
+ // ---------------------------------------------------------------------------
571
+ // Pedidos de compra (v0.5.1)
572
+ // ---------------------------------------------------------------------------
573
+ /**
574
+ * Lista paginada de pedidos de compra con diseño mixto de filtros.
575
+ *
576
+ * Endpoint: GET /pcmp/v2/pedidos
577
+ *
578
+ * DISEÑO MIXTO: el endpoint combina 4 query params nativos con filtros FIQL
579
+ * adicionales vía `rquery`.
580
+ *
581
+ * Filtros que van como **query params nativos** (NO FIQL):
582
+ * - `empresa` (4c) → `codEmpresa`
583
+ * - `codProveedor` (≤10c) → `codProveedor`
584
+ * - `fechaPedidoDesde` (ISO date) → `desdeFecha`
585
+ * - `fechaPedidoHasta` (ISO date) → `hastaFecha`
586
+ *
587
+ * Filtros que van como **FIQL en `rquery`** (campos `ALCC_*`):
588
+ * - `numPedido` → `ALCC_NUMDOC==N`
589
+ * - `codDocumento` → `ALCC_CODDOC==XXXX`
590
+ * - `delegacion` → `ALCC_DELEG==XXXX`
591
+ * - `formaPago` → `ALCC_FPAGO==XXX`
592
+ * - `tipoIva` → `ALCC_TIPO_IVA==XXXX`
593
+ * - `codCliente` → `ALCC_COD_CLIENTE==X`
594
+ * - `codInstalador` → `ALCC_COD_INSTALADOR==X`
595
+ * - `codMantenedor` → `ALCC_COD_MANTENEDOR==X`
596
+ * - `fechaEntregaDesde` → `ALCC_FCHENTREGA=ge=YYYY-MM-DD`
597
+ * - `fechaEntregaHasta` → `ALCC_FCHENTREGA=le=YYYY-MM-DD`
598
+ * - `referencia` → `ALCC_REFERENCIA==X`
599
+ * - `estado` → expresión compuesta sobre `ALCC_PED_BLOQ` y `ALCC_PED_RECIB`
600
+ * (ver `buildEstadoPedidoFiql` y comentario empírico en `EstadoPedidoEnum`)
601
+ *
602
+ * @param opts - Filtros tipados del schema `ListPedidosCompraFiltersSchema`.
603
+ * @returns Lista paginada de pedidos de compra.
604
+ */
605
+ async listPedidosCompra(opts = {}) {
606
+ const url = new URL('https://placeholder/pcmp/v2/pedidos');
607
+ // Paginación
608
+ if (opts.items !== undefined)
609
+ url.searchParams.set('items', String(opts.items));
610
+ if (opts.page !== undefined)
611
+ url.searchParams.set('page', String(opts.page));
612
+ // Query params nativos (NO FIQL)
613
+ if (opts.empresa !== undefined)
614
+ url.searchParams.set('codEmpresa', opts.empresa);
615
+ if (opts.codProveedor !== undefined)
616
+ url.searchParams.set('codProveedor', opts.codProveedor);
617
+ if (opts.fechaPedidoDesde !== undefined)
618
+ url.searchParams.set('desdeFecha', opts.fechaPedidoDesde);
619
+ if (opts.fechaPedidoHasta !== undefined)
620
+ url.searchParams.set('hastaFecha', opts.fechaPedidoHasta);
621
+ // Filtros FIQL — cada parte se genera por separado y se une con ";"
622
+ const fiqlParts = [];
623
+ // Scalar equality filters: agrupar en un objeto y construir de una vez
624
+ const eqGroup = {};
625
+ if (opts.codDocumento !== undefined)
626
+ eqGroup['ALCC_CODDOC'] = opts.codDocumento;
627
+ if (opts.delegacion !== undefined)
628
+ eqGroup['ALCC_DELEG'] = opts.delegacion;
629
+ if (opts.formaPago !== undefined)
630
+ eqGroup['ALCC_FPAGO'] = opts.formaPago;
631
+ if (opts.tipoIva !== undefined)
632
+ eqGroup['ALCC_TIPO_IVA'] = opts.tipoIva;
633
+ if (opts.codCliente !== undefined)
634
+ eqGroup['ALCC_COD_CLIENTE'] = opts.codCliente;
635
+ if (opts.codInstalador !== undefined)
636
+ eqGroup['ALCC_COD_INSTALADOR'] = opts.codInstalador;
637
+ if (opts.codMantenedor !== undefined)
638
+ eqGroup['ALCC_COD_MANTENEDOR'] = opts.codMantenedor;
639
+ if (opts.referencia !== undefined)
640
+ eqGroup['ALCC_REFERENCIA'] = opts.referencia;
641
+ // numPedido es number → convertir a string para FIQL
642
+ if (opts.numPedido !== undefined)
643
+ eqGroup['ALCC_NUMDOC'] = String(opts.numPedido);
644
+ const eqFiql = buildFiql(eqGroup);
645
+ if (eqFiql)
646
+ fiqlParts.push(eqFiql);
647
+ // Date range filters for fechaEntrega (same field, two possible operators)
648
+ if (opts.fechaEntregaDesde !== undefined) {
649
+ fiqlParts.push(buildFiql({ ALCC_FCHENTREGA: { op: 'ge', value: opts.fechaEntregaDesde } }));
650
+ }
651
+ if (opts.fechaEntregaHasta !== undefined) {
652
+ fiqlParts.push(buildFiql({ ALCC_FCHENTREGA: { op: 'le', value: opts.fechaEntregaHasta } }));
653
+ }
654
+ // Estado — genera expresión FIQL compuesta sobre ALCC_PED_BLOQ + ALCC_PED_RECIB
655
+ if (opts.estado !== undefined) {
656
+ fiqlParts.push(buildEstadoPedidoFiql(opts.estado));
657
+ }
658
+ appendRquery(url, fiqlParts.join(';'));
659
+ const path = url.pathname + (url.search ? url.search : '');
660
+ const data = await this.get(path);
661
+ return { items: data.items, total: Number(data.total) };
662
+ }
663
+ /**
664
+ * Detalle de un pedido de compra por `idReg` opaco.
665
+ *
666
+ * Endpoint: GET /pcmp/v2/pedidos/{idreg}
667
+ *
668
+ * La respuesta es un objeto compuesto `{ VoPedidosCompraCab, cabecera_proveedor, lineas[] }`
669
+ * que se devuelve sin aplanar para conservar la estructura original del API.
670
+ *
671
+ * @param idReg - Identificador opaco (puede contener caracteres como "==" de base64).
672
+ * @returns Objeto compuesto con la cabecera, datos del proveedor y líneas del pedido.
673
+ */
674
+ async getPedidoCompra(idReg) {
675
+ return this.get(`/pcmp/v2/pedidos/${encodeURIComponent(idReg)}`);
676
+ }
677
+ /**
678
+ * Detalle de un documento de cartera de clientes por `idReg` opaco.
679
+ *
680
+ * Endpoint: GET /pcar/v1/cartera-clientes/{idreg}
681
+ *
682
+ * @param idReg - Identificador opaco (base64) del documento.
683
+ * @returns Documento de cartera completo.
684
+ */
685
+ async getCarteraCliente(idReg) {
686
+ return this.get(`/pcar/v1/cartera-clientes/${encodeURIComponent(idReg)}`);
687
+ }
688
+ // ---------------------------------------------------------------------------
689
+ // Proveedores (v0.5.0)
690
+ // ---------------------------------------------------------------------------
691
+ /**
692
+ * Lista paginada de proveedores con filtros FIQL.
693
+ *
694
+ * El filtro `activo` mapea a FECHA_BAJA:
695
+ * - `true` → FECHA_BAJA es nula (proveedor activo). Se usa el operador `=is-null=true` si el
696
+ * API lo soporta; como fallback se omite el filtro y se filtra post-process.
697
+ * En Freemática el convenio estándar es: activo=true → `FECHA_BAJA==null` (FIQL `==null`).
698
+ * - `false` → FECHA_BAJA tiene valor (proveedor de baja).
699
+ *
700
+ * El filtro `nombre` usa operador FIQL `=lk=` para búsqueda parcial (LIKE).
701
+ *
702
+ * Endpoint: GET /pgrl/v2/proveedores
703
+ */
704
+ async listProveedores(opts = {}) {
705
+ const url = new URL('https://placeholder/pgrl/v2/proveedores');
706
+ if (opts.items !== undefined)
707
+ url.searchParams.set('items', String(opts.items));
708
+ if (opts.page !== undefined)
709
+ url.searchParams.set('page', String(opts.page));
710
+ // Construir filtros FIQL — activo se traduce a FECHA_BAJA
711
+ const filters = {
712
+ COD_PRO: opts.codProveedor,
713
+ COD_GRUPO_PRO: opts.grupoProveedor,
714
+ NIF: opts.nif,
715
+ CMP_TIPO_IDENT: opts.tipoIdent,
716
+ COD_PROVINCIA: opts.codProvincia,
717
+ COD_PAIS: opts.codPais,
718
+ };
719
+ if (opts.nombre !== undefined) {
720
+ filters['NOMBRE_PRO'] = { op: 'lk', value: opts.nombre };
721
+ }
722
+ if (opts.activo === true) {
723
+ // Activos: FECHA_BAJA nula. FIQL: FECHA_BAJA==null
724
+ filters['FECHA_BAJA'] = 'null';
725
+ }
726
+ else if (opts.activo === false) {
727
+ // Dados de baja: FECHA_BAJA tiene valor (not null). FIQL: FECHA_BAJA!=null
728
+ filters['FECHA_BAJA'] = { op: 'ne', value: 'null' };
729
+ }
730
+ const fiql = buildFiql(filters);
731
+ appendRquery(url, fiql);
732
+ const path = url.pathname + (url.search ? url.search : '');
733
+ const data = await this.get(path);
734
+ return { items: data.items, total: Number(data.total) };
735
+ }
736
+ // ---------------------------------------------------------------------------
737
+ // Facturas de ventas (v0.5.0)
738
+ // ---------------------------------------------------------------------------
739
+ /**
740
+ * Lista paginada de cabeceras de facturas de ventas con filtros FIQL.
741
+ *
742
+ * Endpoint: GET /pven/v1/facturas-cabecera
743
+ *
744
+ * @param opts - Filtros tipados del schema `ListFacturasCabeceraFiltersSchema`.
745
+ * @returns Lista paginada de cabeceras de facturas.
746
+ */
747
+ async listFacturasCabecera(opts = {}) {
748
+ const url = new URL('/pven/v1/facturas-cabecera', 'http://x');
749
+ if (opts.items !== undefined)
750
+ url.searchParams.set('items', String(opts.items));
751
+ if (opts.page !== undefined)
752
+ url.searchParams.set('page', String(opts.page));
753
+ const fiqlParts = [];
754
+ // Standard equality filters
755
+ const eqGroup = {};
756
+ if (opts.empresa !== undefined)
757
+ eqGroup['FVC_EMP'] = opts.empresa;
758
+ if (opts.codCliente !== undefined)
759
+ eqGroup['FVC_CODAUX'] = opts.codCliente;
760
+ if (opts.representante !== undefined)
761
+ eqGroup['FVC_CODREP'] = opts.representante;
762
+ if (opts.serie !== undefined)
763
+ eqGroup['FVC_SERFAC'] = opts.serie;
764
+ if (opts.numFactura !== undefined)
765
+ eqGroup['FVC_NUMFAC'] = opts.numFactura;
766
+ if (opts.formaPago !== undefined)
767
+ eqGroup['FVC_CODFPAG'] = opts.formaPago;
768
+ if (opts.delegacion !== undefined)
769
+ eqGroup['FVC_DELEG'] = opts.delegacion;
770
+ const eqFiql = buildFiql(eqGroup);
771
+ if (eqFiql)
772
+ fiqlParts.push(eqFiql);
773
+ // Date range filters
774
+ if (opts.fechaFacturaDesde !== undefined)
775
+ fiqlParts.push(buildFiql({ FVC_FECFAC: { op: 'ge', value: opts.fechaFacturaDesde } }));
776
+ if (opts.fechaFacturaHasta !== undefined)
777
+ fiqlParts.push(buildFiql({ FVC_FECFAC: { op: 'le', value: opts.fechaFacturaHasta } }));
778
+ // Boolean: traspasadoContabilidad
779
+ if (opts.traspasadoContabilidad !== undefined) {
780
+ fiqlParts.push(buildFiql({ FVC_TRSCONT: opts.traspasadoContabilidad ? 'S' : 'N' }));
781
+ }
782
+ appendRquery(url, fiqlParts.join(';'));
783
+ const path = url.pathname + (url.search ? url.search : '');
784
+ const data = await this.get(path);
785
+ return { items: data.items, total: Number(data.total) };
786
+ }
787
+ /**
788
+ * Detalle de un proveedor por `idReg` opaco.
789
+ *
790
+ * Endpoint: GET /pgrl/v2/proveedores/{idReg}
791
+ */
792
+ async getProveedor(idReg) {
793
+ return this.get(`/pgrl/v2/proveedores/${encodeURIComponent(idReg)}`);
794
+ }
795
+ // ---------------------------------------------------------------------------
796
+ // Localizaciones (v0.5.0)
797
+ // ---------------------------------------------------------------------------
798
+ /**
799
+ * Lista paginada de localizaciones de cobro de clientes.
800
+ *
801
+ * Filtros soportados: codCliente (COD_CLI), grupoCliente (GRUPO_CLI), formaPago (COD_FORMA_COBRO).
802
+ *
803
+ * Endpoint: GET /pgrl/v2/localizaciones-cobro-clientes
804
+ */
805
+ async listLocalizacionesCobroClientes(opts = {}) {
806
+ return this.listResourceWithFiql('/pgrl/v2/localizaciones-cobro-clientes', opts, {
807
+ COD_CLI: opts.codCliente,
808
+ GRUPO_CLI: opts.grupoCliente,
809
+ COD_FORMA_COBRO: opts.formaPago,
810
+ });
811
+ }
812
+ /**
813
+ * Lista paginada de localizaciones de pago de proveedores.
814
+ *
815
+ * Filtros soportados: codProveedor (COD_PRO), grupoProveedor (COD_GRUPO_PRO), formaPago (COD_FORMA_PAGO).
816
+ *
817
+ * Endpoint: GET /pgrl/v2/localizaciones-pago-proveedores
818
+ */
819
+ async listLocalizacionesPagoProveedores(opts = {}) {
820
+ return this.listResourceWithFiql('/pgrl/v2/localizaciones-pago-proveedores', opts, {
821
+ COD_PRO: opts.codProveedor,
822
+ COD_GRUPO_PRO: opts.grupoProveedor,
823
+ COD_FORMA_PAGO: opts.formaPago,
824
+ });
825
+ }
826
+ /**
827
+ * Lista paginada de localizaciones de servicio de clientes.
828
+ *
829
+ * Filtros soportados: codCliente (COD_CLI), grupoCliente (GRUPO_CLI), codPais (COD_PAIS),
830
+ * codProvincia (COD_PROVINCIA), representante (COD_REPRES), activo (FECHA_BAJA nulo/no nulo).
831
+ *
832
+ * Endpoint: GET /pgrl/v2/localizaciones-servicio-clientes
833
+ */
834
+ async listLocalizacionesServicioClientes(opts = {}) {
835
+ const fiqlFilters = {
836
+ COD_CLI: opts.codCliente,
837
+ GRUPO_CLI: opts.grupoCliente,
838
+ COD_PAIS: opts.codPais,
839
+ COD_PROVINCIA: opts.codProvincia,
840
+ COD_REPRES: opts.representante,
841
+ };
842
+ if (opts.activo === true) {
843
+ fiqlFilters['FECHA_BAJA'] = 'null';
844
+ }
845
+ else if (opts.activo === false) {
846
+ fiqlFilters['FECHA_BAJA'] = { op: 'ne', value: 'null' };
847
+ }
848
+ return this.listResourceWithFiql('/pgrl/v2/localizaciones-servicio-clientes', opts, fiqlFilters);
849
+ }
850
+ /**
851
+ * Detalle de una factura de ventas (cabecera) por `idReg` opaco.
852
+ *
853
+ * Endpoint: GET /pven/v1/facturas-cabecera/{idreg}
854
+ *
855
+ * @param idReg - Identificador opaco (base64) de la factura.
856
+ * @returns Factura cabecera completa.
857
+ */
858
+ async getFacturaCabecera(idReg) {
859
+ return this.get(`/pven/v1/facturas-cabecera/${encodeURIComponent(idReg)}`);
860
+ }
861
+ /**
862
+ * Lista paginada de líneas de una factura de ventas con filtros FIQL.
863
+ *
864
+ * Endpoint: GET /pven/v1/facturas-cabecera/{idreg}/lineas
865
+ *
866
+ * @param idReg - idReg opaco de la factura cabecera.
867
+ * @param opts - Filtros tipados del schema `ListFacturaLineasFiltersSchema`.
868
+ * @returns Lista paginada de líneas de factura.
869
+ */
870
+ async listFacturaLineas(idReg, opts = {}) {
871
+ const url = new URL(`/pven/v1/facturas-cabecera/${encodeURIComponent(idReg)}/lineas`, 'http://x');
872
+ if (opts.items !== undefined)
873
+ url.searchParams.set('items', String(opts.items));
874
+ if (opts.page !== undefined)
875
+ url.searchParams.set('page', String(opts.page));
876
+ const fiqlGroup = {};
877
+ if (opts.codArticulo !== undefined)
878
+ fiqlGroup['FVL_CODART'] = opts.codArticulo;
879
+ if (opts.codFamilia !== undefined)
880
+ fiqlGroup['FVL_CODFAM'] = opts.codFamilia;
881
+ if (opts.codSubfamilia !== undefined)
882
+ fiqlGroup['FVL_CODSFAM'] = opts.codSubfamilia;
883
+ if (opts.delegacion !== undefined)
884
+ fiqlGroup['FVL_DELEG'] = opts.delegacion;
885
+ appendRquery(url, buildFiql(fiqlGroup));
886
+ const path = url.pathname + (url.search ? url.search : '');
887
+ const data = await this.get(path);
888
+ return { items: data.items, total: Number(data.total) };
889
+ }
890
+ /**
891
+ * Lista paginada de líneas de IVA de una factura de ventas con filtros FIQL.
892
+ *
893
+ * Endpoint: GET /pven/v1/facturas-cabecera/{idreg}/iva
894
+ *
895
+ * @param idReg - idReg opaco de la factura cabecera.
896
+ * @param opts - Filtros tipados del schema `ListFacturaIvaFiltersSchema`.
897
+ * @returns Lista paginada de líneas de IVA.
898
+ */
899
+ async listFacturaIva(idReg, opts = {}) {
900
+ const url = new URL(`/pven/v1/facturas-cabecera/${encodeURIComponent(idReg)}/iva`, 'http://x');
901
+ if (opts.items !== undefined)
902
+ url.searchParams.set('items', String(opts.items));
903
+ if (opts.page !== undefined)
904
+ url.searchParams.set('page', String(opts.page));
905
+ const fiqlGroup = {};
906
+ if (opts.tipoIva !== undefined)
907
+ fiqlGroup['FVI_TIPIVA'] = opts.tipoIva;
908
+ appendRquery(url, buildFiql(fiqlGroup));
909
+ const path = url.pathname + (url.search ? url.search : '');
910
+ const data = await this.get(path);
911
+ return { items: data.items, total: Number(data.total) };
912
+ }
913
+ /**
914
+ * Lista paginada de vencimientos de una factura de ventas con filtros FIQL.
915
+ *
916
+ * Endpoint: GET /pven/v1/facturas-cabecera/{idreg}/vencimientos
917
+ *
918
+ * @param idReg - idReg opaco de la factura cabecera.
919
+ * @param opts - Filtros tipados del schema `ListFacturaVencimientosFiltersSchema`.
920
+ * @returns Lista paginada de vencimientos.
921
+ */
922
+ async listFacturaVencimientos(idReg, opts = {}) {
923
+ const url = new URL(`/pven/v1/facturas-cabecera/${encodeURIComponent(idReg)}/vencimientos`, 'http://x');
924
+ if (opts.items !== undefined)
925
+ url.searchParams.set('items', String(opts.items));
926
+ if (opts.page !== undefined)
927
+ url.searchParams.set('page', String(opts.page));
928
+ const fiqlParts = [];
929
+ if (opts.modoPago !== undefined)
930
+ fiqlParts.push(buildFiql({ FVV_CODMPAG: opts.modoPago }));
931
+ if (opts.fechaVencimientoDesde !== undefined)
932
+ fiqlParts.push(buildFiql({ FVV_FECVCTO: { op: 'ge', value: opts.fechaVencimientoDesde } }));
933
+ if (opts.fechaVencimientoHasta !== undefined)
934
+ fiqlParts.push(buildFiql({ FVV_FECVCTO: { op: 'le', value: opts.fechaVencimientoHasta } }));
935
+ appendRquery(url, fiqlParts.join(';'));
936
+ const path = url.pathname + (url.search ? url.search : '');
937
+ const data = await this.get(path);
938
+ return { items: data.items, total: Number(data.total) };
939
+ }
940
+ // ---------------------------------------------------------------------------
941
+ // Facturas electrónicas (Facturae/EDICOM/FACe) (v0.5.1 / TD-154)
942
+ // ---------------------------------------------------------------------------
943
+ /**
944
+ * Lista paginada de facturas electrónicas con filtros nativos (NO FIQL).
945
+ *
946
+ * Endpoint: GET /pven/v1/facturas
947
+ *
948
+ * Diferente de listFacturasCabecera: expone las facturas firmadas/enviadas vía
949
+ * Facturae, EDICOM y/o FACe. Los filtros son query params nativos del API.
950
+ *
951
+ * @param opts - Filtros nativos: empresa, codCliente, fechaDesde/Hasta, estado, leido + paginación.
952
+ * @returns Lista paginada de facturas electrónicas con campos FACED_*.
953
+ */
954
+ async listFacturasElectronicas(opts) {
955
+ const url = new URL('https://placeholder/pven/v1/facturas');
956
+ if (opts.items !== undefined)
957
+ url.searchParams.set('items', String(opts.items));
958
+ if (opts.page !== undefined)
959
+ url.searchParams.set('page', String(opts.page));
960
+ if (opts.empresa !== undefined)
961
+ url.searchParams.set('empresa', opts.empresa);
962
+ if (opts.codCliente !== undefined)
963
+ url.searchParams.set('cliente', opts.codCliente);
964
+ if (opts.fechaDesde !== undefined)
965
+ url.searchParams.set('fechaIni', opts.fechaDesde);
966
+ if (opts.fechaHasta !== undefined)
967
+ url.searchParams.set('fechaFin', opts.fechaHasta);
968
+ if (opts.estado !== undefined)
969
+ url.searchParams.set('estado', opts.estado);
970
+ if (opts.leido !== undefined)
971
+ url.searchParams.set('leido', opts.leido ? '1' : '0');
972
+ if (opts.order !== undefined)
973
+ url.searchParams.set('order', opts.order);
974
+ const path = url.pathname + (url.search ? url.search : '');
975
+ const data = await this.get(path);
976
+ return { items: data.items, total: Number(data.total) };
977
+ }
978
+ // ---------------------------------------------------------------------------
979
+ // Albaranes de ventas (v0.5.1 / TD-155)
980
+ // ---------------------------------------------------------------------------
981
+ /**
982
+ * Lista paginada de albaranes de ventas con filtros nativos del endpoint.
983
+ *
984
+ * Endpoint: GET /pven/v2/albaranes-ventas
985
+ *
986
+ * El endpoint usa exclusivamente query params nativos para los filtros
987
+ * principales (no FIQL). `codEmpresa` es obligatorio según el spec OpenAPI.
988
+ */
989
+ async listAlbaranesVentas(opts) {
990
+ const url = new URL('https://placeholder/pven/v2/albaranes-ventas');
991
+ if (opts.items !== undefined)
992
+ url.searchParams.set('items', String(opts.items));
993
+ if (opts.page !== undefined)
994
+ url.searchParams.set('page', String(opts.page));
995
+ url.searchParams.set('codEmpresa', opts.empresa);
996
+ if (opts.delegacion !== undefined)
997
+ url.searchParams.set('codDelegacion', opts.delegacion);
998
+ if (opts.codCliente !== undefined)
999
+ url.searchParams.set('codCliente', opts.codCliente);
1000
+ if (opts.codDocumento !== undefined)
1001
+ url.searchParams.set('codDocumento', opts.codDocumento);
1002
+ if (opts.fechaDesde !== undefined)
1003
+ url.searchParams.set('desdeFecha', opts.fechaDesde);
1004
+ if (opts.fechaHasta !== undefined)
1005
+ url.searchParams.set('hastaFecha', opts.fechaHasta);
1006
+ if (opts.order !== undefined)
1007
+ url.searchParams.set('order', opts.order);
1008
+ const path = url.pathname + (url.search ? url.search : '');
1009
+ const data = await this.get(path);
1010
+ return { items: data.items, total: Number(data.total) };
1011
+ }
1012
+ /**
1013
+ * Detalle de una factura electrónica por `idReg` opaco.
1014
+ *
1015
+ * Endpoint: GET /pven/v1/facturas/{idreg}
1016
+ *
1017
+ * @param idReg - Identificador opaco (base64) de la factura electrónica.
1018
+ * @returns Objeto con los campos de la factura electrónica.
1019
+ */
1020
+ async getFacturaElectronica(idReg) {
1021
+ return this.get(`/pven/v1/facturas/${encodeURIComponent(idReg)}`);
1022
+ }
1023
+ /**
1024
+ * Recupera el documento (PDF o XML Facturae firmado) de una factura electrónica.
1025
+ *
1026
+ * Endpoint: GET /pven/v1/facturas/{idreg}/documento
1027
+ *
1028
+ * La API devuelve un JSON (VoFacturasDocumento) con los documentos codificados en base64
1029
+ * en los campos FACED_DOCUMENTO_PDF, FACED_DOCUMENTO_XML, etc.
1030
+ *
1031
+ * Si el tamaño del JSON supera FREEMATICA_MAX_RESPONSE_SIZE_MB, la tool aplica el
1032
+ * guardrail de tamaño y devuelve `{ truncated: true, warning, sizeBytes }` sin el cuerpo.
1033
+ *
1034
+ * @param idReg - Identificador opaco de la factura.
1035
+ * @param opts - Opciones: documentType (tipo de documento), actualizaLeido (marcar como leído).
1036
+ * @returns Objeto VoFacturasDocumento con campos FACED_DOCUMENTO_*.
1037
+ */
1038
+ async getFacturaDocumento(idReg, opts = {}) {
1039
+ const maxMb = loadMaxResponseSizeMb();
1040
+ const maxBytes = maxMb * 1024 * 1024;
1041
+ const url = new URL(`https://placeholder/pven/v1/facturas/${encodeURIComponent(idReg)}/documento`);
1042
+ if (opts.documentType !== undefined)
1043
+ url.searchParams.set('documentType', opts.documentType);
1044
+ if (opts.actualizaLeido !== undefined)
1045
+ url.searchParams.set('actualiza-leido', String(opts.actualizaLeido));
1046
+ const path = url.pathname + (url.search ? url.search : '');
1047
+ const data = await this.get(path);
1048
+ // Validación de tamaño: los documentos base64 pueden ser grandes
1049
+ const serialized = JSON.stringify(data);
1050
+ const sizeBytes = Buffer.byteLength(serialized, 'utf8');
1051
+ if (sizeBytes > maxBytes) {
1052
+ logger.warn({ sizeBytes, maxBytes, maxMb, idReg }, 'getFacturaDocumento: respuesta excede MAX_RESPONSE_SIZE_MB, aplicando guardrail');
1053
+ return {
1054
+ truncated: true,
1055
+ warning: [
1056
+ `El documento supera el límite de ${maxMb} MB (${Math.round(sizeBytes / 1024 / 1024 * 10) / 10} MB).`,
1057
+ `Para descargar el documento, usa la URL del sistema Freemática directamente.`,
1058
+ ].join(' '),
1059
+ sizeBytes,
1060
+ };
1061
+ }
1062
+ return { truncated: false, ...data };
1063
+ }
1064
+ /**
1065
+ * Log de auditoría de una factura electrónica (envío, recepción, aceptación AAPP, etc.).
1066
+ *
1067
+ * Endpoint: GET /pven/v1/facturas/{idreg}/log
1068
+ *
1069
+ * @param idReg - Identificador opaco de la factura.
1070
+ * @param opts - Opciones de paginación.
1071
+ * @returns Lista paginada de eventos de auditoría con campos FVCTRLE_*.
1072
+ */
1073
+ async getFacturaLog(idReg, opts = {}) {
1074
+ return this.listResource(`/pven/v1/facturas/${encodeURIComponent(idReg)}/log`, opts);
1075
+ }
1076
+ /**
1077
+ * Configuración de la integración EDICOM (empresa, aplicación, URLs, credenciales).
1078
+ *
1079
+ * Endpoint: GET /pven/v1/facturas/edicominfo
1080
+ *
1081
+ * Devuelve una lista de configuraciones EDICOM (campos EDICOM_*).
1082
+ * NOTA: incluye campos sensibles (EDICOM_USER, EDICOM_PASSWORD). Usar con precaución.
1083
+ *
1084
+ * @param opts - Opciones de paginación.
1085
+ * @returns Lista de configuraciones EDICOM con campos EDICOM_*.
1086
+ */
1087
+ async getEdicomInfo(opts = {}) {
1088
+ return this.listResource('/pven/v1/facturas/edicominfo', opts);
1089
+ }
1090
+ /**
1091
+ * Descarga masiva de documentos de facturas electrónicas (PDF/XML/ERROR).
1092
+ *
1093
+ * Endpoint: GET /pven/v1/facturas/download
1094
+ *
1095
+ * Permite recuperar múltiples documentos filtrando por identificadores y tipo.
1096
+ * Los documentos se devuelven con campos VoEFraDoc (empresa, serie, numFra, documentoPdf, etc.).
1097
+ *
1098
+ * @param opts - Filtros: documents (IDs separados por comas), documentType (PDF/XML/ERROR).
1099
+ * @returns Lista de documentos con campos VoEFraDoc.
1100
+ */
1101
+ async listFacturasDocumentos(opts = {}) {
1102
+ const url = new URL('https://placeholder/pven/v1/facturas/download');
1103
+ if (opts.documents !== undefined)
1104
+ url.searchParams.set('documents', opts.documents);
1105
+ if (opts.documentType !== undefined)
1106
+ url.searchParams.set('documentType', opts.documentType);
1107
+ const path = url.pathname + (url.search ? url.search : '');
1108
+ const data = await this.get(path);
1109
+ return { items: data.items, total: Number(data.total) };
1110
+ }
1111
+ // ---------------------------------------------------------------------------
1112
+ // Albaranes de ventas — detalle (v0.5.1 / TD-155)
1113
+ // ---------------------------------------------------------------------------
1114
+ /**
1115
+ * Detalle de un albarán de venta por `idReg` opaco.
1116
+ *
1117
+ * Endpoint: GET /pven/v2/albaranes-ventas/{idReg}
1118
+ *
1119
+ * @param idReg - Identificador opaco (base64) del albarán.
1120
+ * @returns Objeto completo del albarán de venta.
1121
+ */
1122
+ async getAlbaranVenta(idReg) {
1123
+ return this.get(`/pven/v2/albaranes-ventas/${encodeURIComponent(idReg)}`);
1124
+ }
1125
+ // ---------------------------------------------------------------------------
1126
+ // Albaranes-facturas (v0.5.1 / TD-155)
1127
+ // ---------------------------------------------------------------------------
1128
+ /**
1129
+ * Lista paginada de vinculaciones albarán↔factura.
1130
+ *
1131
+ * Endpoint: GET /pven/v2/albaranes-facturas
1132
+ *
1133
+ * El filtro `idReg` es un query param nativo. Los filtros empresa, serie,
1134
+ * numFactura y codCliente se envían como FIQL en el parámetro `rQuery`.
1135
+ *
1136
+ * Mapeo de filtros FIQL:
1137
+ * | opts.empresa | FVCA_CODEMP |
1138
+ * | opts.serie | FVCA_SERIEFRA |
1139
+ * | opts.numFactura | FVCA_NUMFRA |
1140
+ * | opts.codCliente | FVCA_CODCLI |
1141
+ *
1142
+ * @param opts - Opciones de paginación y filtros.
1143
+ * @returns Lista paginada de vinculaciones albarán-factura.
1144
+ */
1145
+ async listAlbaranesFactura(opts) {
1146
+ const url = new URL('https://placeholder/pven/v2/albaranes-facturas');
1147
+ // Paginación
1148
+ if (opts.items !== undefined)
1149
+ url.searchParams.set('items', String(opts.items));
1150
+ if (opts.page !== undefined)
1151
+ url.searchParams.set('page', String(opts.page));
1152
+ // idReg como query param nativo
1153
+ if (opts.idReg !== undefined)
1154
+ url.searchParams.set('idReg', opts.idReg);
1155
+ // Filtros FIQL
1156
+ const fiqlGroup = {};
1157
+ if (opts.empresa !== undefined)
1158
+ fiqlGroup['FVCA_CODEMP'] = opts.empresa;
1159
+ if (opts.serie !== undefined)
1160
+ fiqlGroup['FVCA_SERIEFRA'] = opts.serie;
1161
+ if (opts.numFactura !== undefined)
1162
+ fiqlGroup['FVCA_NUMFRA'] = opts.numFactura;
1163
+ if (opts.codCliente !== undefined)
1164
+ fiqlGroup['FVCA_CODCLI'] = opts.codCliente;
1165
+ appendRquery(url, buildFiql(fiqlGroup));
1166
+ const path = url.pathname + (url.search ? url.search : '');
1167
+ const data = await this.get(path);
1168
+ return { items: data.items, total: Number(data.total) };
1169
+ }
1170
+ /**
1171
+ * Detalle de una vinculación albarán↔factura por `idReg` opaco.
1172
+ *
1173
+ * Endpoint: GET /pven/v2/albaranes-facturas/{idReg}
1174
+ *
1175
+ * @param idReg - Identificador opaco (base64) del registro de vinculación.
1176
+ * @returns Objeto completo de la vinculación albarán-factura.
1177
+ */
1178
+ async getAlbaranFactura(idReg) {
1179
+ return this.get(`/pven/v2/albaranes-facturas/${encodeURIComponent(idReg)}`);
1180
+ }
1181
+ // ---------------------------------------------------------------------------
1182
+ // Resultados de facturación — vigilancia (v0.5.1 / TD-155)
1183
+ // ---------------------------------------------------------------------------
1184
+ /**
1185
+ * Lista paginada de resultados del proceso batch de facturación de vigilancia.
1186
+ *
1187
+ * Endpoint: GET /pvss/v1/facturacion-resultados
1188
+ *
1189
+ * Todos los filtros se envían como FIQL en el parámetro `rquery`.
1190
+ * El parámetro `order` es nativo del endpoint.
1191
+ *
1192
+ * Mapeo de filtros FIQL:
1193
+ * | opts.empresa | FACT_EMP |
1194
+ * | opts.delegacion | FACT_DELEG |
1195
+ * | opts.codCliente | FACT_COD_CLI |
1196
+ * | opts.calendario | FACT_CAL |
1197
+ * | opts.mes | FACT_MES |
1198
+ * | opts.contrato | FACT_CTRT |
1199
+ * | opts.servicio | FACT_SERV |
1200
+ * | opts.tipoFac | FACT_TIPFAC |
1201
+ * | opts.traspasado | FACT_TRASP |
1202
+ *
1203
+ * @param opts - Opciones de paginación y filtros FIQL.
1204
+ * @returns Lista paginada de resultados de facturación.
1205
+ */
1206
+ async listResultadosFacturacion(opts) {
1207
+ const url = new URL('https://placeholder/pvss/v1/facturacion-resultados');
1208
+ // Paginación
1209
+ if (opts.items !== undefined)
1210
+ url.searchParams.set('items', String(opts.items));
1211
+ if (opts.page !== undefined)
1212
+ url.searchParams.set('page', String(opts.page));
1213
+ // order es param nativo
1214
+ if (opts.order !== undefined)
1215
+ url.searchParams.set('order', opts.order);
1216
+ // Filtros FIQL
1217
+ const fiqlGroup = {};
1218
+ if (opts.empresa !== undefined)
1219
+ fiqlGroup['FACT_EMP'] = opts.empresa;
1220
+ if (opts.delegacion !== undefined)
1221
+ fiqlGroup['FACT_DELEG'] = opts.delegacion;
1222
+ if (opts.codCliente !== undefined)
1223
+ fiqlGroup['FACT_COD_CLI'] = opts.codCliente;
1224
+ if (opts.calendario !== undefined)
1225
+ fiqlGroup['FACT_CAL'] = opts.calendario;
1226
+ if (opts.mes !== undefined)
1227
+ fiqlGroup['FACT_MES'] = String(opts.mes);
1228
+ if (opts.contrato !== undefined)
1229
+ fiqlGroup['FACT_CTRT'] = opts.contrato;
1230
+ if (opts.servicio !== undefined)
1231
+ fiqlGroup['FACT_SERV'] = opts.servicio;
1232
+ if (opts.tipoFac !== undefined)
1233
+ fiqlGroup['FACT_TIPFAC'] = opts.tipoFac;
1234
+ if (opts.traspasado !== undefined)
1235
+ fiqlGroup['FACT_TRASP'] = opts.traspasado;
1236
+ appendRquery(url, buildFiql(fiqlGroup));
1237
+ const path = url.pathname + (url.search ? url.search : '');
1238
+ const data = await this.get(path);
1239
+ return { items: data.items, total: Number(data.total) };
1240
+ }
1241
+ // ---------------------------------------------------------------------------
1242
+ // Internal helpers
1243
+ // ---------------------------------------------------------------------------
1244
+ async listResource(path, opts) {
1245
+ const params = new URLSearchParams();
1246
+ if (opts.items !== undefined)
1247
+ params.set('items', String(opts.items));
1248
+ if (opts.page !== undefined)
1249
+ params.set('page', String(opts.page));
1250
+ const query = params.toString();
1251
+ const url = query ? `${path}?${query}` : path;
1252
+ const data = await this.get(url);
1253
+ return { items: data.items, total: Number(data.total) };
1254
+ }
1255
+ /**
1256
+ * Helper genérico para endpoints de lista que soportan paginación + filtros FIQL
1257
+ * vía query param `rquery`.
1258
+ *
1259
+ * @param path - Path base del endpoint (sin query string).
1260
+ * @param opts - Opciones de paginación.
1261
+ * @param fiqlFields - Mapa de nombre de campo FIQL → valor. Los valores `undefined` se omiten.
1262
+ * @returns Lista de items con total.
1263
+ */
1264
+ async listResourceWithFiql(path, opts, fiqlFields) {
1265
+ const url = new URL(`https://placeholder${path}`);
1266
+ if (opts.items !== undefined)
1267
+ url.searchParams.set('items', String(opts.items));
1268
+ if (opts.page !== undefined)
1269
+ url.searchParams.set('page', String(opts.page));
1270
+ const fiql = buildFiql(fiqlFields);
1271
+ appendRquery(url, fiql);
1272
+ const fullPath = url.pathname + (url.search ? url.search : '');
1273
+ const data = await this.get(fullPath);
1274
+ return { items: data.items, total: Number(data.total) };
1275
+ }
1276
+ }
1277
+ // ---------------------------------------------------------------------------
1278
+ // Helpers privados del módulo
1279
+ // ---------------------------------------------------------------------------
1280
+ /**
1281
+ * Calcula el siguiente prefijo léxico para emular un prefix match FIQL.
1282
+ *
1283
+ * Para un prefijo como "430", el rango [430, 431) captura todos los valores
1284
+ * que comienzan por "430". Si el último carácter es el carácter máximo
1285
+ * unicode (U+FFFF), no se puede calcular un límite superior, por lo que se
1286
+ * devuelve `null` (sólo se aplica el límite inferior).
1287
+ *
1288
+ * @param prefix - Prefijo a extender.
1289
+ * @returns Siguiente prefijo léxico, o `null` si no aplica.
1290
+ */
1291
+ function buildNextPrefix(prefix) {
1292
+ if (prefix.length === 0)
1293
+ return null;
1294
+ const last = prefix.charCodeAt(prefix.length - 1);
1295
+ if (last >= 0xfffe)
1296
+ return null;
1297
+ return prefix.slice(0, -1) + String.fromCharCode(last + 1);
1298
+ }
1299
+ /**
1300
+ * Trunca un array de items para que su JSON serializado no supere `maxBytes`.
1301
+ *
1302
+ * Utiliza búsqueda binaria para encontrar eficientemente el número máximo de
1303
+ * items que caben dentro del límite.
1304
+ *
1305
+ * @param items - Items originales.
1306
+ * @param maxBytes - Límite en bytes.
1307
+ * @returns Subarray de items que cabe dentro del límite.
1308
+ */
1309
+ function truncateItemsToLimit(items, maxBytes) {
1310
+ if (items.length === 0)
1311
+ return [];
1312
+ // Búsqueda binaria: encuentra el máximo n tal que JSON([0..n-1]) <= maxBytes
1313
+ let lo = 0;
1314
+ let hi = items.length;
1315
+ while (lo < hi) {
1316
+ const mid = Math.floor((lo + hi) / 2);
1317
+ const size = Buffer.byteLength(JSON.stringify(items.slice(0, mid + 1)), 'utf8');
1318
+ if (size <= maxBytes) {
1319
+ lo = mid + 1;
1320
+ }
1321
+ else {
1322
+ hi = mid;
1323
+ }
1324
+ }
1325
+ return items.slice(0, lo);
1326
+ }
1327
+ //# sourceMappingURL=freematica-client.js.map