@fullqueso/mcp-bc-gastos 1.27.0 → 1.31.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.
Files changed (64) hide show
  1. package/.env.example +8 -0
  2. package/CHANGELOG.md +86 -0
  3. package/README.md +3 -1
  4. package/config/company-config.js +8 -1
  5. package/config/income-accounts.js +29 -0
  6. package/lib/bc-client.js +23 -3
  7. package/package.json +1 -1
  8. package/server.js +5 -0
  9. package/tools/account-transactions.js +1 -1
  10. package/tools/anomaly-detection.js +1 -1
  11. package/tools/auditoria/bank-ledger-entries.js +1 -1
  12. package/tools/auditoria/bank-reconciliation-report.js +1 -1
  13. package/tools/auditoria/find-potential-matches.js +1 -1
  14. package/tools/auditoria/gl-account-entries.js +1 -1
  15. package/tools/auditoria/list-bank-accounts.js +1 -1
  16. package/tools/auditoria/pm-receipts.js +1 -1
  17. package/tools/auditoria/reconcile-pos-sales.js +1 -1
  18. package/tools/auditoria/reconciliation-status.js +1 -1
  19. package/tools/auditoria/suggest-journal-entries.js +1 -1
  20. package/tools/auditoria/unmatched-ledger-entries.js +1 -1
  21. package/tools/auditoria/unmatched-statement-lines.js +1 -1
  22. package/tools/cierre-mensual/generate-closing-journal.js +1 -1
  23. package/tools/cierre-mensual/get-match-results.js +1 -1
  24. package/tools/cierre-mensual/get-questionnaire.js +1 -1
  25. package/tools/cierre-mensual/reconcile-with-bc.js +1 -1
  26. package/tools/cierre-mensual/start-month-closing.js +1 -1
  27. package/tools/cierre-mensual/submit-answers.js +1 -1
  28. package/tools/cobranzas/collection-status.js +1 -1
  29. package/tools/cobranzas/customer-balances.js +1 -1
  30. package/tools/cobranzas/customer-ledger.js +1 -1
  31. package/tools/cobranzas/customer-list.js +1 -1
  32. package/tools/cobranzas/open-payables.js +1 -1
  33. package/tools/cobranzas/open-receivables.js +1 -1
  34. package/tools/cobranzas/vendor-ledger.js +1 -1
  35. package/tools/efficiency-ratios.js +1 -1
  36. package/tools/expense-analysis.js +1 -1
  37. package/tools/expense-details.js +1 -1
  38. package/tools/financials/cash-flow.js +1 -1
  39. package/tools/financials/index.js +3 -3
  40. package/tools/get-crm-rate.js +109 -0
  41. package/tools/get-exchange-rate.js +1 -1
  42. package/tools/inventario/inventory-by-location.js +1 -1
  43. package/tools/inventario/inventory-change.js +1 -1
  44. package/tools/inventario/inventory-levels.js +1 -1
  45. package/tools/inventario/item-card.js +1 -1
  46. package/tools/inventario/item-cost-trend.js +1 -1
  47. package/tools/inventario/item-ledger-entries.js +1 -1
  48. package/tools/inventario/item-value-entries.js +1 -1
  49. package/tools/list-vendors.js +1 -1
  50. package/tools/multi-payment/draft-payables.js +1 -1
  51. package/tools/multi-payment/draft-receivables.js +1 -1
  52. package/tools/multi-payment/draft-summary.js +1 -1
  53. package/tools/payroll/cost-model.js +51 -0
  54. package/tools/payroll/employees.js +30 -6
  55. package/tools/payroll/payroll-documents.js +197 -13
  56. package/tools/payroll/payroll-lines.js +58 -34
  57. package/tools/reports/manager-report.js +1 -1
  58. package/tools/trends.js +1 -1
  59. package/tools/vendor-transactions.js +1 -1
  60. package/tools/ventas/item-sales-detail.js +1 -1
  61. package/tools/ventas/product-performance.js +1 -1
  62. package/tools/ventas/sales-analysis.js +2 -2
  63. package/tools/ventas/sales-store-comparison.js +5 -1
  64. package/utils/sales-aggregation.js +35 -1
package/.env.example CHANGED
@@ -13,6 +13,14 @@ BC_ENVIRONMENT=production
13
13
  BC_COMPANY_FQ01=guid-for-store-fq01
14
14
  BC_COMPANY_FQ28=guid-for-store-fq28
15
15
  BC_COMPANY_FQ88=guid-for-store-fq88
16
+ BC_COMPANY_FQFR=guid-for-franquicias
16
17
 
17
18
  # Optional
18
19
  LOG_LEVEL=info
20
+
21
+ # Per-request BC fetch timeout (ms). A hung request is aborted + retried instead
22
+ # of blocking the MCP forever (BUG #1 fix). Default 90000. Raise for heavy reports.
23
+ BC_FETCH_TIMEOUT_MS=90000
24
+
25
+ # CRM rate endpoint (get_crm_rate) — opcional, default abajo
26
+ CRM_RATE_BASE=https://crm-rate.fullqueso.com/api/v1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,91 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## [1.31.0] — 2026-07-01
4
+
5
+ ### Added — KPIs de nómina operativa: tienda vs eventos, activos vs pagados
6
+ Contratos existentes intactos (solo se agregan campos). Todo USD. Verificado contra BC real (mayo 2026, FQ01/FQ28/FQ88/FQFR).
7
+
8
+ - **`paid_employees` en `get_payroll_documents` (con `include_employee_count=true`).** Además de `summary.unique_employees`, ahora `summary.paid_employees`: lista agregada por empleado en el período con `{ employee_code, employee_name, cedula, cost_usd, lines, payroll_types:[...], classification }`. Permite cruzar activos vs pagados sin leer las líneas doc por doc.
9
+ - **Deduplicación por cédula.** `unique_employees` y `paid_employees` deduplican por `cedula` cuando existe (una persona puede tener un código SEMANAL y otro EVENTO → se consolidan costo y líneas en un registro). `summary.duplicate_codes: [{ cedula, codes:[...] }]` expone los casos multi-código para limpieza en BC. Si `cedula` es null se trata el código como persona única (no se fusiona por nombre). El consolidado (`store="all"`) re-deduplica también entre tiendas (misma cédula en dos tiendas = una persona).
10
+ - Resolución de cédula en 2 pasos: si un código tiene cédula en alguna línea, las líneas con cédula en blanco de ese código heredan la cédula (evita partir un código en dos personas). Verificado mayo 2026: 0 splits, 0 merges; 82 personas = 58 con cédula (1:1) + 24 códigos sin cédula en BC (data-quality, tratados como únicos).
11
+ - **Clasificación evento/operativo por empleado** (relevante FQ01): `classification` = `"evento"` (todos sus pagos EVENTO), `"operativo"` (ningún EVENTO — SEMANAL/VACACIONES/LIQUIDACION), `"mixto"` (tiene EVENTO y no-EVENTO). Evita marcar como "inactivo no declarado" a quien solo trabaja eventos.
12
+ - **Buckets de nómina tienda vs eventos** en `summary` (per-store y consolidado): `store_payroll` = Σ `cost_usd` de docs SEMANAL + VACACIONES + LIQUIDACION; `event_payroll` = Σ EVENTO. `total_cost_usd` se mantiene igual (gerencia queda solo ahí). Constantes `STORE_PAYROLL_TYPES`/`EVENT_PAYROLL_TYPES` en `tools/payroll/cost-model.js`.
13
+
14
+ ### Added — Denominador de ventas correcto para el KPI (get_sales_analysis / compare_sales_by_store)
15
+ - **`revenue_store`, `revenue_events`, `revenue_intercompany`, `revenue_non_sales` + `revenue_total`** en `summary` (y por tienda en `compare_sales_by_store`), además del `total_revenue_usd` actual (sin cambios). Identidad: `revenue_total = revenue_store + revenue_intercompany + revenue_events + revenue_non_sales`.
16
+ - **`revenue_store = revenue_total − intercompañía − eventos − no-venta`.** Se excluye del denominador de tienda la venta intercompañía, la venta de eventos y el ingreso **no-venta** (FX gains + rebajas de compra) — este último por recomendación aceptada por FP.
17
+ - **Cuentas configurables (no inline), por tienda** en `config/income-accounts.js`, verificadas contra el CoA real:
18
+ - `INTERCOMPANY_REVENUE_ACCOUNTS = ['40310']` (tiendas operativas; FQ01/FQ88, FQ28=0).
19
+ - `EVENT_REVENUE_ACCOUNTS = ['40180','40700']` (solo FQ01 con movimiento).
20
+ - `NON_SALES_REVENUE_ACCOUNTS = ['40580','40540','40550']` (dif. cambiario + descuentos/rebajas de compra).
21
+ - **FQFR** usa `FQFR_INTERCOMPANY_REVENUE_ACCOUNTS = ['40420','40430','40660','40930']` (royalties/fees del master de franquicia, NO 40310) vía `getRevenueClassification(storeCode)` → sus buckets salen correctos aunque el KPI no aplique.
22
+ - Lógica en `splitRevenueBuckets(glRevenue, total, storeCode)` (`utils/sales-aggregation.js`); `calculateSummary` ahora recibe `storeCode` (los dos tools de ventas lo pasan por tienda).
23
+ - **KPI** (lo calcula el consumidor cruzando ambos tools): `nomina_tienda_pct = store_payroll / revenue_store`; `nomina_eventos_pct = event_payroll / revenue_events` (**null si `revenue_events = 0`** → solo FQ01). **FQFR excluido por completo** del KPI (`KPI_PAYROLL_STORES = ['FQ01','FQ28','FQ88']`).
24
+ - Verificado mayo 2026 (2 fases, `tests/verify-payroll-sales-kpi.js`):
25
+ - **Estricto** (− interco − eventos), coincide con la validación de FP: FQ01 8.36% tienda / 31.02% eventos (rev_store 86,987.43); FQ28 10.22% (77,407.51); FQ88 8.75% (150,122.32).
26
+ - **Refinado** (además − no-venta): FQ01 8.36% (86,940.37); FQ28 10.25% (77,146.08); FQ88 8.80% (149,322.16).
27
+
28
+ ### Nota operativa
29
+ - **Reiniciar Claude Desktop** tras el publish para que los tools recarguen (G-002).
30
+
31
+ ---
32
+
33
+ ## [1.30.0] — 2026-07-01
34
+
35
+ ### Fixed
36
+ - **BUG #1 — `get_employees`/`get_payroll_documents` con `store="all"` podían colgar el MCP indefinidamente.** Causa raíz: `fetch()` no tenía timeout → una sola petición colgada nunca resolvía, y como el consolidado usaba `Promise.all`, una tienda colgada tumbaba todo el call (aparecía como "MCP no responde").
37
+ - **`lib/bc-client.js` — timeout por petición (AbortController).** Cada intento de `fetchWithRetry` aborta a los `BC_FETCH_TIMEOUT_MS` (default **90000**, env-tunable) y reintenta; al agotar reintentos lanza un error claro de timeout en vez de colgarse. Cubre token + todas las llamadas v2.0/OData/custom (afecta a las 54 tools; el default generoso no toca ninguna llamada legítima — todas las observadas <1s).
38
+ - **Consolidación multi-tienda resiliente.** `get_payroll_documents` y `get_employees` con `store="all"` ahora usan `Promise.allSettled`: una tienda que falle/timeoutee devuelve data parcial + un array `errors[]` por tienda, nunca cuelga ni bota el consolidado.
39
+ - Verificado: per-store discovery <500ms; el timeout aborta+reintenta (3x) y lanza error en ~6s con `BC_FETCH_TIMEOUT_MS=1`.
40
+
41
+ ### Changed / Added — KPIs de nómina operativa
42
+ - **#2/#3 — Costo total en USD por documento (`total_cost_usd`) robusto a drafts sin distribuir.** Nuevo módulo `tools/payroll/cost-model.js` como **fuente única de verdad del costo**. Regla (verificada contra 448 docs reales): `total_cost_usd = totalGrossUSD (>0) → si no, bonos-equivalente USD → si no, neto (MAX, no suma)`. El campo `cost_basis` (`gross`|`bonuses`|`net`) hace auditable de dónde salió el número, y `net_distributed:false` marca los drafts cuyo neto aún no se reparte por método de pago (elimina el parche `max(neto,bonos)`).
43
+ - **Por qué la suma `net_usd + net_ves/tasa` estaba mal:** VACACIONES/LIQUIDACION contabilizan el MISMO monto en `totalNetUSD` **y** `totalNetVES` → esa suma **duplica** el costo. `totalGrossUSD` es el costo real y ya está en el header (antes no se seleccionaba).
44
+ - Expuestos en cada documento: `total_cost_usd`, `cost_basis`, `net_distributed`, `total_gross_usd`, `total_deductions_usd`. Agregado `total_cost_usd` a `summary` y `consolidated`.
45
+ - **#4 — Enums de `payroll_type` corregidos a los valores reales de BC + `exclude_managerial`.** El enum viejo `[SEMANAL|QUINCENAL|MENSUAL]` estaba mal (QUINCENAL/MENSUAL no existen).
46
+ - `get_payroll_documents`: `SEMANAL, GERENCIAL, GERENCIAL_CIERRE, EVENTO, VACACIONES, LIQUIDACION`. Nuevo flag `exclude_managerial` → excluye GERENCIAL + GERENCIAL_CIERRE server-side (nómina operativa en un solo call).
47
+ - `get_employees`: `SEMANAL, GERENCIAL, EVENTO`. Nuevo flag `exclude_managerial` → excluye `payrollType GERENCIAL` + `employeeType Management`.
48
+ - **#5 — `summary.by_type` ahora trae costo, no solo conteo.** De `{ TIPO: n }` a `{ TIPO: { count, lines, cost_usd } }` (per-store y consolidado).
49
+ - **#6 — Headcount único de empleados pagados en el periodo.** Nuevo flag `include_employee_count` en `get_payroll_documents` → `summary.unique_employees` (y `consolidated.unique_employees`). Lee las líneas de los docs filtrados en lotes de OR (no doc-por-doc). Distingue el headcount real de `total_employees_lines` (que duplica al mismo empleado por cada corrida semanal).
50
+ - **#7 — Costo por empleado en USD en `get_payroll_lines`.** Expuesto `total_ves_equivalent` (campo de la API antes sin usar) y `cost_usd` por línea = `totalVESEquivalent / tasa` (la tasa se lee del header en 1 llamada extra). `summary` trae `total_cost_usd`, `total_ves_equivalent`, `payroll_type`, `status`, `exchange_rate`.
51
+ - **Nota (L-001):** la API `fqPayrollLines` **no** expone días/horas trabajados ni FTE por línea → no se pueden calcular KPIs de productividad laboral sin ampliar la extensión de BC. Documentado, no fabricado.
52
+
53
+ ### Nota operativa
54
+ - **Reiniciar Claude Desktop** tras el publish para que los tools recarguen (G-002).
55
+
56
+ ---
57
+
58
+ ## [1.29.0] — 2026-06-21
59
+
60
+ ### Added
61
+ - **`get_crm_rate` — tasa de cambio Bs/USD desde el CRM de Full Queso (tasa de referencia, hora Caracas).** Complementa a `get_exchange_rate` (que trae la tasa BCV contable vigente en BC): este tool consulta el endpoint del CRM `https://crm-rate.fullqueso.com` y expone tanto la tasa **USDT/Binance** (paralelo) como la **BCV** (oficial), histórica o del día.
62
+ - **Dos monedas con sinónimos:** `coin` acepta `USDT`/`BINANCE` (paralelo, mismo valor) y `VES`/`BCV` (oficial). La descripción dispara con "tasa USDT, tasa Binance, dólar Binance, tasa paralelo/paralela, tasa BCV, tasa oficial, tasa del día, tasa histórica".
63
+ - **Histórica vs del día automático:** con `date` (YYYY-MM-DD) → `GET /api/v1/history/price?coin={VES|USDT}&date=...`; sin `date` → `GET /api/v1/coin/{bcv|usdt}` (momento de la consulta).
64
+ - Maneja `400` (coin/date inválidos → error claro), `404` (sin registro → `price: null` + mensaje) y fallo de conexión. Precio redondeado a 2 decimales; respuesta incluye `queried_at_caracas` y `source`.
65
+ - Base URL configurable vía env **`CRM_RATE_BASE`** (default `https://crm-rate.fullqueso.com/api/v1`). El endpoint no requiere auth de BC → tool autónomo (ignora `bcClient`).
66
+ - Nuevo `tools/get-crm-rate.js`; registrado en `server.js` (import + lista de tools + switch).
67
+ - **Verificado contra el endpoint real (21-jun-2026):** USDT del día ≈798.35; BCV del día 612.43; histórica USDT 19-jun 805.40; histórica VES 19-jun **607.39** (coincide con el ejemplo del doc); 404 (1990) y coin inválido (DOGE) manejados. `node --check` OK.
68
+
69
+ ### Nota operativa
70
+ - **Reiniciar Claude Desktop** tras el publish para que el tool recargue (G-002).
71
+
72
+ ---
73
+
74
+ ## [1.28.0] — 2026-06-12
75
+
76
+ ### Added
77
+ - **Cuarta compañía: `FQFR` — Full Queso Franquicias, C.A.** El MCP solo exponía FQ01/FQ28/FQ88; ahora la compañía de Franquicias está disponible en todas las herramientas.
78
+ - **Datos BC** (descubiertos del endpoint `/companies`): `companyId` (GUID) = `30e63c27-6336-f011-9a4a-000d3afe29c3`; `name` OData V4 = `Full Queso Franquicias` (→ `companyName: 'Full%20Queso%20Franquicias'`).
79
+ - Nueva entrada `FQFR` en `config/company-config.js` (`getStores()`) y agregada a `ALL_STORE_CODES` → `"all"` ahora incluye Franquicias en consolidados/comparativas.
80
+ - Nueva env var **`BC_COMPANY_FQFR`** (opcional, como FQ28/FQ88) en `.env`, `.env.example` y README. La validación de `server.js` no cambia (solo FQ01 es obligatoria).
81
+ - **52 herramientas** tenían el `enum` de tiendas hardcodeado (`['FQ01','FQ28','FQ88'(,'all')]`) — esto rechazaba `FQFR` en la validación del schema MCP. Se agregó `'FQFR'` a los 52 enums (39 sin `'all'`, 13 con `'all'`).
82
+ - **Verificado contra BC real:** token OK; API v2.0 (GUID) → 200; OData V4 (nombre) → 200; `getGLEntries` ingresos 2025 de FQFR → 116 asientos; `resolveStores(['all'])` → `FQ01,FQ28,FQ88,FQFR`. `node --check` OK en todos los .js.
83
+
84
+ ### Nota operativa
85
+ - **Reiniciar Claude Desktop** tras el publish para que los tools recarguen con FQFR (G-002).
86
+
87
+ ---
88
+
3
89
  ## [1.27.0] — 2026-06-05
4
90
 
5
91
  ### Changed
package/README.md CHANGED
@@ -33,7 +33,8 @@ Add to your `claude_desktop_config.json`:
33
33
  "BC_ENVIRONMENT": "production",
34
34
  "BC_COMPANY_FQ01": "company-guid-fq01",
35
35
  "BC_COMPANY_FQ28": "company-guid-fq28",
36
- "BC_COMPANY_FQ88": "company-guid-fq88"
36
+ "BC_COMPANY_FQ88": "company-guid-fq88",
37
+ "BC_COMPANY_FQFR": "company-guid-franquicias"
37
38
  }
38
39
  }
39
40
  }
@@ -181,6 +182,7 @@ All APIs share the same OAuth 2.0 credentials (Azure AD client credentials flow)
181
182
  | `BC_COMPANY_FQ01` | Yes | Company GUID for store FQ01 |
182
183
  | `BC_COMPANY_FQ28` | No | Company GUID for store FQ28 |
183
184
  | `BC_COMPANY_FQ88` | No | Company GUID for store FQ88 |
185
+ | `BC_COMPANY_FQFR` | No | Company GUID for Franquicias (FQFR) |
184
186
  | `LOG_LEVEL` | No | `debug`, `info`, `warn`, `error` (default: `info`) |
185
187
 
186
188
  ## Chart of Accounts
@@ -24,10 +24,17 @@ function getStores() {
24
24
  shortName: 'Candelaria',
25
25
  location: 'La Candelaria, Caracas',
26
26
  },
27
+ FQFR: {
28
+ companyId: process.env.BC_COMPANY_FQFR,
29
+ companyName: 'Full%20Queso%20Franquicias',
30
+ name: 'FQFR Franquicias',
31
+ shortName: 'Franquicias',
32
+ location: 'Franquicias',
33
+ },
27
34
  };
28
35
  }
29
36
 
30
- export const ALL_STORE_CODES = ['FQ01', 'FQ28', 'FQ88'];
37
+ export const ALL_STORE_CODES = ['FQ01', 'FQ28', 'FQ88', 'FQFR'];
31
38
 
32
39
  export function resolveStores(storeCodes) {
33
40
  const stores = getStores();
@@ -25,6 +25,35 @@ export const COGS_ACCOUNTS = {
25
25
  57020: { name: 'Costos Diversos', nameEn: 'Diverse Costs' },
26
26
  };
27
27
 
28
+ // ── Revenue classification for the store-payroll KPI denominator ─────────────
29
+ // Verified against the real CoA (tests/discover-revenue-accounts.js, May 2026):
30
+ // 40310 "Ventas Materia Prima otras tiendas" = intercompany de tienda (FQ01/FQ88,
31
+ // FQ28=0); su costo par es 50110 (see LESSONS L-011).
32
+ // 40180 "Ventas Eventos - NEN" + 40700 "Ventas Eventos" = event sales (solo FQ01).
33
+ // 40580 "Ganancia por dif. cambiario" + 40540/40550 "Descuentos/Rebajas en Compras"
34
+ // = ingreso NO-venta → fuera del denominador de tienda (confirmado por FP).
35
+ // revenue_store = revenue_total − intercompany − events − non_sales.
36
+ export const INTERCOMPANY_REVENUE_ACCOUNTS = ['40310'];
37
+ export const EVENT_REVENUE_ACCOUNTS = ['40180', '40700'];
38
+ export const NON_SALES_REVENUE_ACCOUNTS = ['40580', '40540', '40550'];
39
+
40
+ // FQFR (master de franquicia): su intercompañía usa cuentas propias (royalties/fees),
41
+ // NO 40310, y no tiene ventas de evento de tienda. El KPI de nómina/ventas NO aplica a
42
+ // FQFR (ver KPI_PAYROLL_STORES) — esta clasificación es solo para que sus buckets de
43
+ // venta salgan correctos si alguien consulta ventas de FQFR.
44
+ export const FQFR_INTERCOMPANY_REVENUE_ACCOUNTS = ['40420', '40430', '40660', '40930'];
45
+
46
+ // Tiendas a las que aplica el KPI nómina/ventas (FQFR excluido por completo).
47
+ export const KPI_PAYROLL_STORES = ['FQ01', 'FQ28', 'FQ88'];
48
+
49
+ // Cuentas de clasificación de ingreso por tienda (interco/eventos/no-venta).
50
+ export function getRevenueClassification(storeCode) {
51
+ if (storeCode === 'FQFR') {
52
+ return { intercompany: FQFR_INTERCOMPANY_REVENUE_ACCOUNTS, events: [], nonSales: NON_SALES_REVENUE_ACCOUNTS };
53
+ }
54
+ return { intercompany: INTERCOMPANY_REVENUE_ACCOUNTS, events: EVENT_REVENUE_ACCOUNTS, nonSales: NON_SALES_REVENUE_ACCOUNTS };
55
+ }
56
+
28
57
  // Income group labels for breakdown (by 100-prefix: 40110 → 40100)
29
58
  export const INCOME_GROUP_LABELS = {
30
59
  40100: '40100_ingreso_ventas',
package/lib/bc-client.js CHANGED
@@ -18,6 +18,12 @@ export class BCClient {
18
18
  this._inflightRequests = new Map();
19
19
  this.STORE_DATA_TTL = 5 * 60 * 1000; // 5 minutes
20
20
  this.EXCHANGE_RATE_TTL = 15 * 60 * 1000; // 15 minutes
21
+
22
+ // Per-request hard timeout (ms). Without this a single hung fetch() never
23
+ // rejects → the whole tool call (and, via Promise.all consolidations, the
24
+ // MCP process) hangs forever. Generous default (all observed BC calls are
25
+ // <1s); tunable via env for unusually heavy OData reports. See L-015.
26
+ this.FETCH_TIMEOUT_MS = Number(process.env.BC_FETCH_TIMEOUT_MS) || 90000;
21
27
  }
22
28
 
23
29
  /**
@@ -135,8 +141,12 @@ export class BCClient {
135
141
 
136
142
  async fetchWithRetry(url, options, retries = 3) {
137
143
  for (let attempt = 1; attempt <= retries; attempt++) {
144
+ // Abort a hung request so it fails fast (and retries) instead of blocking
145
+ // forever. Each attempt gets its own controller/timer. See L-015.
146
+ const controller = new AbortController();
147
+ const timer = setTimeout(() => controller.abort(), this.FETCH_TIMEOUT_MS);
138
148
  try {
139
- const response = await fetch(url, options);
149
+ const response = await fetch(url, { ...options, signal: controller.signal });
140
150
  if (response.status === 429 && attempt < retries) {
141
151
  const retryAfter = response.headers.get('Retry-After');
142
152
  const delay = retryAfter ? parseInt(retryAfter) * 1000 : attempt * 3000;
@@ -146,10 +156,20 @@ export class BCClient {
146
156
  }
147
157
  return response;
148
158
  } catch (err) {
149
- if (attempt === retries) throw err;
159
+ const timedOut = err.name === 'AbortError';
160
+ if (attempt === retries) {
161
+ throw timedOut
162
+ ? new Error(`BC request timed out after ${this.FETCH_TIMEOUT_MS}ms (${retries} attempts): ${url}`)
163
+ : err;
164
+ }
150
165
  const delay = attempt * 2000;
151
- logger.warn(`Fetch failed (attempt ${attempt}/${retries}), retrying in ${delay}ms...`);
166
+ logger.warn(
167
+ `Fetch ${timedOut ? `timed out (>${this.FETCH_TIMEOUT_MS}ms)` : 'failed'} ` +
168
+ `(attempt ${attempt}/${retries}), retrying in ${delay}ms...`
169
+ );
152
170
  await new Promise((r) => setTimeout(r, delay));
171
+ } finally {
172
+ clearTimeout(timer);
153
173
  }
154
174
  }
155
175
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fullqueso/mcp-bc-gastos",
3
- "version": "1.27.0",
3
+ "version": "1.31.0",
4
4
  "description": "MCP server for Business Central operational expense analysis, bank reconciliation, POS reconciliation, accounts receivable/payable, multi-payment draft visibility, payroll, inventory cost analysis, and manager reports - Full Queso franchise stores",
5
5
  "main": "server.js",
6
6
  "bin": {
package/server.js CHANGED
@@ -33,6 +33,7 @@ import { accountTransactionsTool, handleAccountTransactions } from './tools/acco
33
33
  import { vendorTransactionsTool, handleVendorTransactions } from './tools/vendor-transactions.js';
34
34
  import { listVendorsTool, handleListVendors } from './tools/list-vendors.js';
35
35
  import { exchangeRateTool, handleGetExchangeRate } from './tools/get-exchange-rate.js';
36
+ import { crmRateTool, handleGetCrmRate } from './tools/get-crm-rate.js';
36
37
 
37
38
  // Auditoria tools (bank reconciliation)
38
39
  import { listBankAccountsTool, handleListBankAccounts } from './tools/auditoria/list-bank-accounts.js';
@@ -138,6 +139,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
138
139
  vendorTransactionsTool,
139
140
  listVendorsTool,
140
141
  exchangeRateTool,
142
+ crmRateTool,
141
143
  // Auditoria (bank reconciliation)
142
144
  listBankAccountsTool,
143
145
  reconciliationStatusTool,
@@ -234,6 +236,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
234
236
  case 'get_exchange_rate':
235
237
  result = await handleGetExchangeRate(bcClient, args);
236
238
  break;
239
+ case 'get_crm_rate':
240
+ result = await handleGetCrmRate(bcClient, args);
241
+ break;
237
242
  // Auditoria (bank reconciliation)
238
243
  case 'list_bank_accounts':
239
244
  result = await handleListBankAccounts(bcClient, args);
@@ -15,7 +15,7 @@ export const accountTransactionsTool = {
15
15
  },
16
16
  store: {
17
17
  type: 'string',
18
- enum: ['FQ01', 'FQ28', 'FQ88'],
18
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'],
19
19
  description: 'Tienda a consultar.',
20
20
  },
21
21
  start_date: {
@@ -28,7 +28,7 @@ export const anomalyDetectionTool = {
28
28
  },
29
29
  stores: {
30
30
  type: 'array',
31
- items: { type: 'string', enum: ['FQ01', 'FQ28', 'FQ88', 'all'] },
31
+ items: { type: 'string', enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR', 'all'] },
32
32
  description: 'Tiendas a analizar.',
33
33
  default: ['all'],
34
34
  },
@@ -9,7 +9,7 @@ export const bankLedgerEntriesTool = {
9
9
  properties: {
10
10
  store: {
11
11
  type: 'string',
12
- enum: ['FQ01', 'FQ28', 'FQ88'],
12
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'],
13
13
  description: 'Tienda a consultar.',
14
14
  },
15
15
  bank_account: {
@@ -12,7 +12,7 @@ export const bankReconciliationReportTool = {
12
12
  properties: {
13
13
  store: {
14
14
  type: 'string',
15
- enum: ['FQ01', 'FQ28', 'FQ88'],
15
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'],
16
16
  description: 'Tienda a consultar.',
17
17
  },
18
18
  bank_account: {
@@ -10,7 +10,7 @@ export const findPotentialMatchesTool = {
10
10
  properties: {
11
11
  store: {
12
12
  type: 'string',
13
- enum: ['FQ01', 'FQ28', 'FQ88'],
13
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'],
14
14
  description: 'Tienda a consultar.',
15
15
  },
16
16
  bank_account: {
@@ -9,7 +9,7 @@ export const glAccountEntriesTool = {
9
9
  properties: {
10
10
  store: {
11
11
  type: 'string',
12
- enum: ['FQ01', 'FQ28', 'FQ88'],
12
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'],
13
13
  description: 'Tienda a consultar.',
14
14
  },
15
15
  gl_account: {
@@ -9,7 +9,7 @@ export const listBankAccountsTool = {
9
9
  properties: {
10
10
  store: {
11
11
  type: 'string',
12
- enum: ['FQ01', 'FQ28', 'FQ88'],
12
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'],
13
13
  description: 'Tienda a consultar.',
14
14
  },
15
15
  },
@@ -9,7 +9,7 @@ export const pmReceiptsTool = {
9
9
  properties: {
10
10
  store: {
11
11
  type: 'string',
12
- enum: ['FQ01', 'FQ28', 'FQ88'],
12
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'],
13
13
  description: 'Tienda a consultar.',
14
14
  },
15
15
  bank_account: {
@@ -414,7 +414,7 @@ export const reconcilePOSSalesTool = {
414
414
  properties: {
415
415
  store: {
416
416
  type: 'string',
417
- enum: ['FQ01', 'FQ28', 'FQ88'],
417
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'],
418
418
  description: 'Tienda a conciliar.',
419
419
  },
420
420
  start_date: {
@@ -10,7 +10,7 @@ export const reconciliationStatusTool = {
10
10
  properties: {
11
11
  store: {
12
12
  type: 'string',
13
- enum: ['FQ01', 'FQ28', 'FQ88'],
13
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'],
14
14
  description: 'Tienda a consultar.',
15
15
  },
16
16
  bank_account: {
@@ -12,7 +12,7 @@ export const suggestJournalEntriesTool = {
12
12
  properties: {
13
13
  store: {
14
14
  type: 'string',
15
- enum: ['FQ01', 'FQ28', 'FQ88'],
15
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'],
16
16
  description: 'Tienda a consultar.',
17
17
  },
18
18
  bank_account: {
@@ -10,7 +10,7 @@ export const unmatchedLedgerEntriesTool = {
10
10
  properties: {
11
11
  store: {
12
12
  type: 'string',
13
- enum: ['FQ01', 'FQ28', 'FQ88'],
13
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'],
14
14
  description: 'Tienda a consultar.',
15
15
  },
16
16
  bank_account: {
@@ -10,7 +10,7 @@ export const unmatchedStatementLinesTool = {
10
10
  properties: {
11
11
  store: {
12
12
  type: 'string',
13
- enum: ['FQ01', 'FQ28', 'FQ88'],
13
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'],
14
14
  description: 'Tienda a consultar.',
15
15
  },
16
16
  bank_account: {
@@ -19,7 +19,7 @@ export const generateClosingJournalTool = {
19
19
  inputSchema: {
20
20
  type: 'object',
21
21
  properties: {
22
- store: { type: 'string', enum: ['FQ01', 'FQ28', 'FQ88'] },
22
+ store: { type: 'string', enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'] },
23
23
  month: { type: 'string', description: 'YYYY-MM' },
24
24
  output_dir: { type: 'string', description: 'Directorio de salida (default: /tmp).' },
25
25
  allow_partial: {
@@ -23,7 +23,7 @@ export const getClosingMatchResultsTool = {
23
23
  inputSchema: {
24
24
  type: 'object',
25
25
  properties: {
26
- store: { type: 'string', enum: ['FQ01', 'FQ28', 'FQ88'] },
26
+ store: { type: 'string', enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'] },
27
27
  month: { type: 'string', description: 'YYYY-MM' },
28
28
  list: {
29
29
  type: 'string',
@@ -13,7 +13,7 @@ export const getClosingQuestionnaireTool = {
13
13
  inputSchema: {
14
14
  type: 'object',
15
15
  properties: {
16
- store: { type: 'string', enum: ['FQ01', 'FQ28', 'FQ88'] },
16
+ store: { type: 'string', enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'] },
17
17
  month: { type: 'string', description: 'YYYY-MM' },
18
18
  bucket: {
19
19
  type: 'string',
@@ -15,7 +15,7 @@ export const reconcileWithBcTool = {
15
15
  inputSchema: {
16
16
  type: 'object',
17
17
  properties: {
18
- store: { type: 'string', enum: ['FQ01', 'FQ28', 'FQ88'] },
18
+ store: { type: 'string', enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'] },
19
19
  month: { type: 'string', description: 'YYYY-MM' },
20
20
  },
21
21
  required: ['store', 'month'],
@@ -19,7 +19,7 @@ export const startMonthClosingTool = {
19
19
  inputSchema: {
20
20
  type: 'object',
21
21
  properties: {
22
- store: { type: 'string', enum: ['FQ01', 'FQ28', 'FQ88'] },
22
+ store: { type: 'string', enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'] },
23
23
  month: {
24
24
  type: 'string',
25
25
  description: 'Mes YYYY-MM (opcional). Si no se especifica, usa el mes más antiguo con statements abiertos.',
@@ -10,7 +10,7 @@ export const submitClosingAnswersTool = {
10
10
  inputSchema: {
11
11
  type: 'object',
12
12
  properties: {
13
- store: { type: 'string', enum: ['FQ01', 'FQ28', 'FQ88'] },
13
+ store: { type: 'string', enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'] },
14
14
  month: { type: 'string', description: 'YYYY-MM' },
15
15
  user: {
16
16
  type: 'string',
@@ -9,7 +9,7 @@ export const collectionStatusTool = {
9
9
  properties: {
10
10
  store: {
11
11
  type: 'string',
12
- enum: ['FQ01', 'FQ28', 'FQ88'],
12
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'],
13
13
  description: 'Tienda a consultar.',
14
14
  },
15
15
  start_date: {
@@ -9,7 +9,7 @@ export const customerBalancesTool = {
9
9
  properties: {
10
10
  store: {
11
11
  type: 'string',
12
- enum: ['FQ01', 'FQ28', 'FQ88'],
12
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'],
13
13
  description: 'Tienda a consultar.',
14
14
  },
15
15
  only_with_balance: {
@@ -9,7 +9,7 @@ export const customerLedgerTool = {
9
9
  properties: {
10
10
  store: {
11
11
  type: 'string',
12
- enum: ['FQ01', 'FQ28', 'FQ88'],
12
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'],
13
13
  description: 'Tienda a consultar.',
14
14
  },
15
15
  start_date: {
@@ -9,7 +9,7 @@ export const customerListTool = {
9
9
  properties: {
10
10
  store: {
11
11
  type: 'string',
12
- enum: ['FQ01', 'FQ28', 'FQ88'],
12
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'],
13
13
  description: 'Tienda a consultar.',
14
14
  },
15
15
  customer_number: {
@@ -9,7 +9,7 @@ export const openPayablesTool = {
9
9
  properties: {
10
10
  store: {
11
11
  type: 'string',
12
- enum: ['FQ01', 'FQ28', 'FQ88'],
12
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'],
13
13
  description: 'Tienda a consultar.',
14
14
  },
15
15
  as_of_date: {
@@ -9,7 +9,7 @@ export const openReceivablesTool = {
9
9
  properties: {
10
10
  store: {
11
11
  type: 'string',
12
- enum: ['FQ01', 'FQ28', 'FQ88'],
12
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'],
13
13
  description: 'Tienda a consultar.',
14
14
  },
15
15
  as_of_date: {
@@ -9,7 +9,7 @@ export const vendorLedgerTool = {
9
9
  properties: {
10
10
  store: {
11
11
  type: 'string',
12
- enum: ['FQ01', 'FQ28', 'FQ88'],
12
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'],
13
13
  description: 'Tienda a consultar.',
14
14
  },
15
15
  start_date: {
@@ -28,7 +28,7 @@ export const efficiencyRatiosTool = {
28
28
  },
29
29
  stores: {
30
30
  type: 'array',
31
- items: { type: 'string', enum: ['FQ01', 'FQ28', 'FQ88', 'all'] },
31
+ items: { type: 'string', enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR', 'all'] },
32
32
  description: 'Tiendas a analizar.',
33
33
  default: ['all'],
34
34
  },
@@ -29,7 +29,7 @@ export const expenseAnalysisTool = {
29
29
  },
30
30
  stores: {
31
31
  type: 'array',
32
- items: { type: 'string', enum: ['FQ01', 'FQ28', 'FQ88', 'all'] },
32
+ items: { type: 'string', enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR', 'all'] },
33
33
  description: 'Tiendas a analizar. Use ["all"] para todas.',
34
34
  default: ['all'],
35
35
  },
@@ -12,7 +12,7 @@ export const expenseDetailsTool = {
12
12
  properties: {
13
13
  store: {
14
14
  type: 'string',
15
- enum: ['FQ01', 'FQ28', 'FQ88'],
15
+ enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR'],
16
16
  description: 'Tienda a consultar.',
17
17
  },
18
18
  period: {
@@ -66,7 +66,7 @@ export const cashFlowTool = {
66
66
  properties: {
67
67
  stores: {
68
68
  type: 'array',
69
- items: { type: 'string', enum: ['FQ01', 'FQ28', 'FQ88', 'all'] },
69
+ items: { type: 'string', enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR', 'all'] },
70
70
  description: 'Tiendas a analizar. Default: ["all"] (FQ01 + FQ28 + FQ88 + consolidado).',
71
71
  default: ['all'],
72
72
  },
@@ -12,7 +12,7 @@ export { cashFlowTool, handleCashFlow } from './cash-flow.js';
12
12
  export const financialStatementsTool = {
13
13
  name: 'get_financial_statements',
14
14
  description:
15
- 'Estado financiero completo (P&L / Profit & Loss) para una o más tiendas Full Queso (FQ01, FQ28, FQ88) en un período. ' +
15
+ 'Estado financiero completo (P&L / Profit & Loss) para una o más tiendas Full Queso (FQ01, FQ28, FQ88, FQFR) en un período. ' +
16
16
  'Ejecuta las queries de Business Central en paralelo (ingresos 40000-49999, COGS 50000-59999, gastos 60000-99999) y ' +
17
17
  'devuelve JSON estructurado: ingresos por cuenta, COGS, margen bruto, gastos por categoría con benchmarks, margen ' +
18
18
  'operativo, ratios clave, score 0-100 e insights accionables. Multi-tienda calcula también el consolidado. ' +
@@ -24,8 +24,8 @@ export const financialStatementsTool = {
24
24
  properties: {
25
25
  stores: {
26
26
  type: 'array',
27
- items: { type: 'string', enum: ['FQ01', 'FQ28', 'FQ88', 'all'] },
28
- description: 'Tiendas a analizar. Default: ["all"] (FQ01 + FQ28 + FQ88 + consolidado).',
27
+ items: { type: 'string', enum: ['FQ01', 'FQ28', 'FQ88', 'FQFR', 'all'] },
28
+ description: 'Tiendas a analizar. Default: ["all"] (FQ01 + FQ28 + FQ88 + FQFR + consolidado).',
29
29
  default: ['all'],
30
30
  },
31
31
  period: {