@fullqueso/mcp-bc-gastos 1.19.0 → 1.23.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 (48) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/config/bank-gl-map.json +5 -5
  3. package/config/bank-keywords.js +35 -0
  4. package/lib/bc-client.js +71 -11
  5. package/package.json +3 -2
  6. package/scripts/diagnose-closing.js +144 -0
  7. package/scripts/generate-month-closing-xlsx.py +325 -0
  8. package/scripts/test-closing-flow.js +71 -0
  9. package/scripts/test-closing-fq01-dec25.js +78 -0
  10. package/server.js +57 -0
  11. package/tools/auditoria/bank-ledger-entries.js +3 -3
  12. package/tools/auditoria/find-potential-matches.js +1 -1
  13. package/tools/auditoria/gl-account-entries.js +1 -1
  14. package/tools/auditoria/list-bank-accounts.js +23 -12
  15. package/tools/auditoria/pm-receipts.js +4 -4
  16. package/tools/auditoria/reconcile-pos-sales.js +11 -9
  17. package/tools/auditoria/unmatched-ledger-entries.js +3 -3
  18. package/tools/auditoria/unmatched-statement-lines.js +2 -2
  19. package/tools/cierre-mensual/fetch-ledger.js +69 -0
  20. package/tools/cierre-mensual/generate-closing-journal.js +111 -0
  21. package/tools/cierre-mensual/get-match-results.js +151 -0
  22. package/tools/cierre-mensual/get-questionnaire.js +283 -0
  23. package/tools/cierre-mensual/index.js +6 -0
  24. package/tools/cierre-mensual/journal-builder.js +219 -0
  25. package/tools/cierre-mensual/matchers/index.js +161 -0
  26. package/tools/cierre-mensual/matchers/match-cross-bank.js +168 -0
  27. package/tools/cierre-mensual/matchers/match-draft-payments.js +178 -0
  28. package/tools/cierre-mensual/matchers/match-feedback-loop.js +24 -0
  29. package/tools/cierre-mensual/matchers/match-keywords.js +65 -0
  30. package/tools/cierre-mensual/matchers/match-open-arap.js +66 -0
  31. package/tools/cierre-mensual/matchers/match-pos-terminal.js +53 -0
  32. package/tools/cierre-mensual/reconcile-with-bc.js +116 -0
  33. package/tools/cierre-mensual/start-month-closing.js +234 -0
  34. package/tools/cierre-mensual/state-store.js +211 -0
  35. package/tools/cierre-mensual/submit-answers.js +106 -0
  36. package/tools/cobranzas/customer-ledger.js +4 -4
  37. package/tools/cobranzas/vendor-ledger.js +4 -4
  38. package/tools/financials/aggregator.js +360 -0
  39. package/tools/financials/cash-flow-html.js +459 -0
  40. package/tools/financials/cash-flow.js +471 -0
  41. package/tools/financials/html-template.js +674 -0
  42. package/tools/financials/index.js +79 -0
  43. package/tools/financials/statements.js +296 -0
  44. package/tools/inventario/item-ledger-entries.js +4 -4
  45. package/tools/inventario/item-value-entries.js +2 -2
  46. package/tools/ventas/index.js +1 -0
  47. package/tools/ventas/item-sales-detail.js +366 -0
  48. package/utils/rate-limiter.js +84 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,56 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## [1.23.0] — 2026-05-31
4
+
5
+ ### Added
6
+ - **Dominio `financials/`: tool `get_cash_flow`** — Free Cash Flow (FCF) por método indirecto para una o más tiendas FQ.
7
+ - Parte del EBIT (reutiliza `fetchStoreFinancials` de `get_financial_statements`), resta impuesto estimado (ISLR 34%, solo si EBIT>0) → NOPAT, ajusta por capital de trabajo (ΔAR, ΔAP, Δinventario) → CFO, resta CapEx (activos fijos 15000-17999) → FCF.
8
+ - Fuentes BC: AR/AP vía Finance Beta (`customerLedgerEntries`/`vendorLedgerEntries`, USD = `amountLocalCurrency`), inventario vía `itemLedgerEntries` (`costAmountActual`), CapEx vía `getGLEntries`. Todas en paralelo (`Promise.all`) junto al P&L.
9
+ - Convención de signos: cada delta = impacto en caja (+ = fuente, − = uso); `net_wc_change = ΔAR+ΔAP+Δinv`; `cfo = nopat + net_wc_change`; `fcf = cfo − capex`. (Resuelve la inconsistencia del spec entre comentarios y fórmula a favor de la convención de impacto-en-caja, autoconsistente.)
10
+ - `fcf_status` (healthy/positive_low/negative_watch/negative_critical), `fcf_bridge` (waterfall) e `insights` automáticos. Consolidado multi-tienda + comparación MoM (con `no_previous_data` para tiendas sin mes anterior).
11
+ - `render_html=true`: dashboard HTML reutilizando el design system de financials + **waterfall CSS** del FCF bridge (sin librerías externas). Guarda en `~/Downloads/FQ_CashFlow_[STORE|Consolidado]_YYYYMM.html`.
12
+ - **`tools/financials/cash-flow.js`** y **`tools/financials/cash-flow-html.js`**. Exportados helpers reutilizables (`CSS`, `fmt`, `esc`, `clamp`, `scoreColorName`, `periodLabel`, `previousMonthRange`) desde `html-template.js`/`statements.js`.
13
+
14
+ ### Notas
15
+ - Si un endpoint BC (AR/AP/inventario/activos fijos) devuelve vacío o falla, ese delta se asume 0 y se agrega un warning a `insights` (no rompe el cálculo).
16
+ - Si una tienda falla, las demás continúan; el error queda en el campo de esa tienda.
17
+ - Tool count: 53 → 54.
18
+
19
+ ---
20
+
21
+ ## [1.22.0] — 2026-05-31
22
+
23
+ ### Added
24
+ - **Dominio `financials/`: tool `get_financial_statements`** — Estado financiero completo (P&L) para una o más tiendas FQ en un período.
25
+ - P&L completo: ingresos por cuenta, COGS, margen bruto, gastos por categoría (con benchmarks), margen operativo.
26
+ - Queries BC en paralelo `Promise.all` (3 tiendas × 3 queries = 9 simultáneas) + exchange rate compartido.
27
+ - Consolidado multi-tienda re-agregando todas las GL entries crudas (consistencia exacta con la suma de tiendas).
28
+ - Template HTML estático (design system FQ: tokens Dark/Light, header rojo, ring de score, toggle, TL;DR, grid KPIs, tabla P&L, barras de gastos, comparativa multi-tienda, `@media print`). `render_html=true` guarda en `~/Downloads` y retorna la ruta.
29
+ - Comparación mes-a-mes con `compare_previous=true`. Si el período anterior no tiene datos (la tienda aún no operaba en BC, p.ej. FQ01 antes de Dic 2025), se marca `no_previous_data: true` en vez de comparar contra cero (evita score/varianzas fabricados).
30
+ - Score 0-100 ponderado por status de 7 ratios vs benchmarks del sector.
31
+ - **`tools/financials/aggregator.js`** — `EXPENSE_CATEGORIES`, `getStatus`, `aggregateIncome`, `aggregateCOGS`, `aggregateExpenses`, `calcRatios`, `calcScore`, `generateInsights`. Reutiliza `config/expense-accounts.js` y `config/benchmarks.js`.
32
+
33
+ ### Notas
34
+ - Reutiliza el `bcClient.getGLEntries()` existente (convención `debitAmount`/`creditAmount`, no el `amount` de Finance Beta). Paginación automática vía `apiCallAllPages`.
35
+ - Bump desde 1.21.0 → 1.22.0 (el spec mencionaba "1.2.0" cuando la versión base era anterior; 1.2.0 sería un downgrade, por eso MINOR sobre la versión real publicada).
36
+ - Tool count: 52 → 53.
37
+
38
+ ### Validado (FQ01 Diciembre 2025)
39
+ - `income.total` = $157,693 · `cogs.total` = $100,158 · `gross_margin.pct` = 36.49% · `expenses.otros.amount` = $19,233 (mermas $16,933) · `operating_income.pct` = 10.6% · `score` ≈ 42.
40
+
41
+ ---
42
+
43
+ ## [1.20.0] - 2026-05-04
44
+
45
+ ### Added
46
+ - **`get_item_sales_detail` tool (ventas)** — Detalle de ventas por SKU (1–50 ítems) con granularidad day/week/month/total y desglose por tienda. Pensado para análisis de promos, canibalización, lanzamientos y SKUs estacionales que caen fuera del top-20 de `get_sales_analysis`. Fuente: `itemLedgerEntries` con `entryType eq 'Sale'` (mismos signos que el screenshot del Item Application Worksheet — quantity y costAmountActual negativos en BC, invertidos a positivos en la salida). Soporta `include_zero_days` para detección de stockouts y series temporales limpias. Multi-company en paralelo (FQ01/FQ28/FQ88).
47
+ - **`bcClient.getItemLedgerSaleEntries(companyId, { items, startDate, endDate })`** — Helper en `lib/bc-client.js` para fetch paginado de ILE Sale entries por lote de ítems.
48
+
49
+ ### Changed
50
+ - Tool count: 45 → 46
51
+
52
+ ---
53
+
3
54
  ## [1.19.0] - 2026-04-13
4
55
 
5
56
  ### Added
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": "1.0",
3
- "last_updated": "2026-03-23",
3
+ "last_updated": "2026-05-17",
4
4
  "description": "Mapeo banco_code (BC Bank Account suffix) → cuenta GL (Chart of Accounts). Usado por POS reconciliation y journal entry suggestions. Cada tienda tiene su propio CoA con bancos distintos.",
5
5
  "stores": {
6
6
  "FQ01": {
@@ -25,12 +25,12 @@
25
25
  "accounts": {
26
26
  "MN0001": { "gl": "18210", "name": "BDV 8139 Bs.", "currency": "VES", "category": "bancos_nacionales" },
27
27
  "MN0002": { "gl": "18220", "name": "Bancrecer 5474 Bs.", "currency": "VES", "category": "bancos_nacionales" },
28
- "MN0028": { "gl": "18290", "name": "UBII 6249 Bs", "currency": "VES", "category": "bancos_nacionales" },
29
- "MN0029": { "gl": "18291", "name": "UBII 6442 * Bs", "currency": "VES", "category": "bancos_nacionales" },
28
+ "MN0028": { "gl": "18290", "name": "UBII 6249 Bs", "currency": "VES", "category": "bancos_nacionales", "_nota": "Punto 2 + Punto 3." },
29
+ "MN0029": { "gl": "18291", "name": "UBII 6442 Bs", "currency": "VES", "category": "bancos_nacionales", "_nota": "Punto 4 + Punto 7." },
30
30
  "MN0030": { "gl": "18130", "name": "Caja tienda ventas Bs.", "currency": "VES", "category": "caja" },
31
31
  "MN0031": { "gl": "18160", "name": "Caja transitoria tienda ventas Bs.", "currency": "VES", "category": "caja" },
32
- "MN0032": { "gl": "18292", "name": "UBII 4 FQ-28", "currency": "VES", "category": "bancos_nacionales" },
33
- "MN0033": { "gl": "18295", "name": "UBII 7 FQ-28", "currency": "VES", "category": "bancos_nacionales" },
32
+ "MN0032": { "gl": "18292", "name": "UBII 4 y 7 (duplicada)", "currency": "VES", "category": "bancos_nacionales", "_nota": "Cuenta duplicada creada por error — NO usar. Punto 4 y Punto 7 van a MN0029. Confirmado 2026-05-17." },
33
+ "MN0033": { "gl": "18295", "name": "UBII 7 bloqueado (duplicada)", "currency": "VES", "category": "bancos_nacionales", "_nota": "Cuenta duplicada creada por error — NO usar. Punto 7 va a MN0029. Confirmado 2026-05-17." },
34
34
  "MN0098": { "gl": "18298", "name": "Operaciones en Tránsito", "currency": "VES", "category": "bancos_nacionales" },
35
35
  "ME0002": { "gl": "18310", "name": "Bancrecer $", "currency": "USD", "category": "bancos_nacionales_divisas" },
36
36
  "ME0005": { "gl": "18410", "name": "Zelle Bofa $ *", "currency": "USD", "category": "bancos_extranjeros" },
@@ -30,6 +30,41 @@ export const BANK_KEYWORDS = [
30
30
  // Transfers
31
31
  { keywords: ['TRANSF RECIBIDA', 'TRANSFERENCIA RECIBIDA'], account: '11000', name: 'Transferencia Recibida' },
32
32
  { keywords: ['TRANSF ENVIADA', 'TRANSFERENCIA ENVIADA'], account: '11000', name: 'Transferencia Enviada' },
33
+ { keywords: ['PAGO RECIBIDO OTROS BANCOS', 'ABONO RECIBIDO OTR BCOS'], account: '11000', name: 'Pago Recibido Otros Bancos' },
34
+ { keywords: ['PAGOMOVIL', 'PAGO MOVIL', 'PAGOMOVILBDV'], account: '11000', name: 'Pago Móvil' },
35
+ { keywords: ['TRANS.CTAS', 'TRASPASO OTRAS CTAS', 'TRF.MB', 'TRF CR INM', 'TRF DR INM'], account: '11000', name: 'Traspaso entre cuentas' },
36
+ { keywords: ['TRANSF EN LINEA OTR BCOS', 'TRANSF EN LINEA'], account: '11000', name: 'Transferencia en línea' },
37
+ { keywords: ['DOMICILIACION DE PAGOS', 'DOMICILIACION'], account: '11000', name: 'Domiciliación' },
38
+
39
+ // UBII / merchant clearing (INM.OB family) — see project memory project_ubii_clearing_account.md
40
+ { keywords: ['CREDITO INM.OB', 'INM.OB'], account: '18290', name: 'UBII Clearing (revisar emparejamiento cross-bank)' },
41
+
42
+ // Loans (líneas de crédito)
43
+ { keywords: ['DESEMBOLSO DE CREDITO'], account: '23100', name: 'Desembolso Crédito Bancario' },
44
+ { keywords: ['PAGO CREDITO', 'PAGO DE CREDITO', 'PAGO PRESTAMO'], account: '23100', name: 'Pago Crédito Bancario' },
45
+ { keywords: ['OP.INTERV', 'OPERACION INTERV', 'INTERV. CAMBIARIA', 'INTERVENCION CAMBIARIA'], account: '11500', name: 'Intervención cambiaria (USD compra/venta)' },
46
+
47
+ // Tax retentions
48
+ { keywords: ['N/D ISLR', 'RETENCION ISLR'], account: '21300', name: 'Retención ISLR' },
49
+ { keywords: ['SENIAT', 'IMPUESTO', 'IVA RETEN'], account: '21100', name: 'Retención IVA / SENIAT' },
50
+
51
+ // Statement fee
52
+ { keywords: ['EMISION DE ESTADO DE CUENTA', 'ESTADO DE CUENTA'], account: '67100', name: 'Comisión emisión estado de cuenta' },
53
+
54
+ // IVSS
55
+ { keywords: ['PAGO IVSS', 'IVSS'], account: '71300', name: 'Pago IVSS' },
56
+
57
+ // Checks
58
+ { keywords: ['CHEQUE PAGADO', 'CHQ', 'CHEQUE'], account: '11000', name: 'Cheque' },
59
+
60
+ // Outbound bank payments (PAGO A OTROS BANCOS / PAGO A TERCEROS)
61
+ { keywords: ['PAGO A OTROS BANCOS', 'PAGO A TERCEROS', 'PAGOS A OTROS BANCOS'], account: '11000', name: 'Pago a otros bancos' },
62
+
63
+ // INM.MB family (Internet/Mobile Banking credit movements, distinct from INM.OB)
64
+ { keywords: ['CREDITO INM.MB', 'INM.MB'], account: '11000', name: 'Movimiento Internet/Mobile Banking' },
65
+
66
+ // Loan opening fees
67
+ { keywords: ['GASTOS APERTURA CREDITOS', 'APERTURA CREDITO'], account: '67100', name: 'Gastos apertura crédito' },
33
68
  ];
34
69
 
35
70
  // Match a bank description against keywords
package/lib/bc-client.js CHANGED
@@ -2,6 +2,7 @@ import { logger } from '../utils/logger.js';
2
2
  import { getExpenseCategory } from '../config/expense-accounts.js';
3
3
  import { INCOME_GROUP_LABELS } from '../config/income-accounts.js';
4
4
  import { resolveStores } from '../config/company-config.js';
5
+ import { bcLimiter } from '../utils/rate-limiter.js';
5
6
 
6
7
  export class BCClient {
7
8
  constructor() {
@@ -19,6 +20,28 @@ export class BCClient {
19
20
  this.EXCHANGE_RATE_TTL = 15 * 60 * 1000; // 15 minutes
20
21
  }
21
22
 
23
+ /**
24
+ * Sanitize a string value before embedding it in an OData $filter expression.
25
+ *
26
+ * OData string literals are delimited by single quotes. An attacker who
27
+ * controls a filter parameter (e.g. accountNo, vendorNo, postingDate) could
28
+ * inject arbitrary filter logic by inserting a single quote. Doubling every
29
+ * single quote is the standard OData escape sequence (identical to SQL).
30
+ *
31
+ * Usage inside a $filter template:
32
+ * `accountNo eq '${this._sanitizeOData(args.accountNo)}'`
33
+ *
34
+ * DOES NOT protect numeric / date literals — those should be validated with
35
+ * Number.isFinite() or a date regex before use.
36
+ *
37
+ * @param {string} value
38
+ * @returns {string}
39
+ */
40
+ _sanitizeOData(value) {
41
+ if (value == null) return '';
42
+ return String(value).replace(/'/g, "''");
43
+ }
44
+
22
45
  _cacheKey(prefix, ...parts) {
23
46
  return `${prefix}:${parts.join(':')}`;
24
47
  }
@@ -93,6 +116,7 @@ export class BCClient {
93
116
  }
94
117
 
95
118
  async apiCall(url) {
119
+ bcLimiter.recordCall(); // throws if rate limit exceeded
96
120
  const token = await this.getToken();
97
121
  logger.debug(`API call: ${url}`);
98
122
 
@@ -130,13 +154,20 @@ export class BCClient {
130
154
  }
131
155
  }
132
156
 
133
- async apiCallAllPages(url) {
157
+ async apiCallAllPages(url, maxPages = 20) {
134
158
  const token = await this.getToken();
135
159
  let allResults = [];
136
160
  let nextUrl = url;
161
+ let pageCount = 0;
137
162
 
138
163
  while (nextUrl) {
139
- logger.debug(`API call (paged): ${nextUrl}`);
164
+ if (pageCount >= maxPages) {
165
+ logger.warn(`apiCallAllPages: maxPages (${maxPages}) reached — results may be truncated. Set maxPages higher if needed.`);
166
+ allResults._truncated = true;
167
+ break;
168
+ }
169
+ bcLimiter.recordCall();
170
+ logger.debug(`API call (paged ${pageCount + 1}/${maxPages}): ${nextUrl}`);
140
171
  const response = await this.fetchWithRetry(nextUrl, {
141
172
  headers: { Authorization: `Bearer ${token}` },
142
173
  });
@@ -149,6 +180,7 @@ export class BCClient {
149
180
  const data = await response.json();
150
181
  allResults = allResults.concat(data.value || []);
151
182
  nextUrl = data['@odata.nextLink'] || null;
183
+ pageCount++;
152
184
  }
153
185
 
154
186
  return allResults;
@@ -240,13 +272,20 @@ export class BCClient {
240
272
  return data.value || data;
241
273
  }
242
274
 
243
- async odataCallAllPages(url) {
275
+ async odataCallAllPages(url, maxPages = 20) {
244
276
  const token = await this.getToken();
245
277
  let allResults = [];
246
278
  let nextUrl = url;
279
+ let pageCount = 0;
247
280
 
248
281
  while (nextUrl) {
249
- logger.debug(`OData call (paged): ${nextUrl}`);
282
+ if (pageCount >= maxPages) {
283
+ logger.warn(`odataCallAllPages: maxPages (${maxPages}) reached — results may be truncated.`);
284
+ allResults._truncated = true;
285
+ break;
286
+ }
287
+ bcLimiter.recordCall();
288
+ logger.debug(`OData call (paged ${pageCount + 1}/${maxPages}): ${nextUrl}`);
250
289
  const response = await this.fetchWithRetry(nextUrl, {
251
290
  headers: { Authorization: `Bearer ${token}` },
252
291
  });
@@ -259,6 +298,7 @@ export class BCClient {
259
298
  const data = await response.json();
260
299
  allResults = allResults.concat(data.value || []);
261
300
  nextUrl = data['@odata.nextLink'] || null;
301
+ pageCount++;
262
302
  }
263
303
 
264
304
  return allResults;
@@ -266,8 +306,10 @@ export class BCClient {
266
306
 
267
307
  // Fetch General Ledger Entries for a specific account range
268
308
  async getGLEntries(companyId, startDate, endDate, accountMin, accountMax) {
309
+ const sd = this._sanitizeOData(startDate);
310
+ const ed = this._sanitizeOData(endDate);
269
311
  const url = this.buildApiUrl(companyId, 'generalLedgerEntries', {
270
- $filter: `postingDate ge ${startDate} and postingDate le ${endDate} and accountNumber ge '${accountMin}' and accountNumber le '${accountMax}'`,
312
+ $filter: `postingDate ge ${sd} and postingDate le ${ed} and accountNumber ge '${accountMin}' and accountNumber le '${accountMax}'`,
271
313
  $select: 'entryNumber,postingDate,documentNumber,accountNumber,description,debitAmount,creditAmount',
272
314
  $orderby: 'postingDate desc',
273
315
  });
@@ -509,13 +551,13 @@ export class BCClient {
509
551
  async getDetailedGLEntries(companyId, filters = {}) {
510
552
  const conditions = [];
511
553
 
512
- if (filters.startDate) conditions.push(`postingDate ge ${filters.startDate}`);
513
- if (filters.endDate) conditions.push(`postingDate le ${filters.endDate}`);
554
+ if (filters.startDate) conditions.push(`postingDate ge ${this._sanitizeOData(filters.startDate)}`);
555
+ if (filters.endDate) conditions.push(`postingDate le ${this._sanitizeOData(filters.endDate)}`);
514
556
  if (filters.accountNumber) {
515
- conditions.push(`accountNumber eq '${filters.accountNumber}'`);
557
+ conditions.push(`accountNumber eq '${this._sanitizeOData(filters.accountNumber)}'`);
516
558
  } else if (filters.accountMin && filters.accountMax) {
517
- conditions.push(`accountNumber ge '${filters.accountMin}'`);
518
- conditions.push(`accountNumber le '${filters.accountMax}'`);
559
+ conditions.push(`accountNumber ge '${this._sanitizeOData(filters.accountMin)}'`);
560
+ conditions.push(`accountNumber le '${this._sanitizeOData(filters.accountMax)}'`);
519
561
  }
520
562
 
521
563
  const params = {
@@ -533,8 +575,10 @@ export class BCClient {
533
575
  // ── Sales Analysis Methods ──────────────────────────────────────
534
576
 
535
577
  async getSalesInvoicesExpanded(companyId, startDate, endDate) {
578
+ const sd = this._sanitizeOData(startDate);
579
+ const ed = this._sanitizeOData(endDate);
536
580
  const url = this.buildApiUrl(companyId, 'salesInvoices', {
537
- $filter: `postingDate ge ${startDate} and postingDate le ${endDate}`,
581
+ $filter: `postingDate ge ${sd} and postingDate le ${ed}`,
538
582
  $orderby: 'postingDate desc',
539
583
  $expand: 'salesInvoiceLines',
540
584
  });
@@ -572,6 +616,22 @@ export class BCClient {
572
616
  return this.apiCallAllPages(url);
573
617
  }
574
618
 
619
+ // Fetch Sale-type Item Ledger Entries for one or more items in a date range.
620
+ // Returns the raw per-entry rows from BC (signs unchanged: quantity & costAmountActual negative).
621
+ async getItemLedgerSaleEntries(companyId, { items, startDate, endDate }) {
622
+ if (!items || items.length === 0) return [];
623
+ const itemFilter = items.length === 1
624
+ ? `itemNumber eq '${items[0]}'`
625
+ : `(${items.map((i) => `itemNumber eq '${i}'`).join(' or ')})`;
626
+
627
+ const url = this.buildApiUrl(companyId, 'itemLedgerEntries', {
628
+ $filter: `${itemFilter} and entryType eq 'Sale' and postingDate ge ${startDate} and postingDate le ${endDate}`,
629
+ $select: 'entryNumber,itemNumber,postingDate,entryType,documentNumber,description,quantity,salesAmountActual,costAmountActual',
630
+ $orderby: 'itemNumber,postingDate',
631
+ });
632
+ return this.apiCallAllPages(url);
633
+ }
634
+
575
635
  async getSalesStoreData(storeCode, startDate, endDate, exchangeRates = null) {
576
636
  const stores = resolveStores([storeCode]);
577
637
  const store = stores[0];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fullqueso/mcp-bc-gastos",
3
- "version": "1.19.0",
3
+ "version": "1.23.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": {
@@ -23,7 +23,8 @@
23
23
  "start": "node server.js",
24
24
  "dev": "nodemon server.js",
25
25
  "test-connection": "node test-connection.js",
26
- "test-tools": "node test-tools.js"
26
+ "test-tools": "node test-tools.js",
27
+ "test-item-sales-detail": "node tests/test-item-sales-detail.js"
27
28
  },
28
29
  "keywords": [
29
30
  "mcp",
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+ // Read-only diagnostic for any (store, month) bank closing snapshot.
3
+ //
4
+ // Usage:
5
+ // node scripts/diagnose-closing.js --store FQ01 --month 2025-12
6
+ // node scripts/diagnose-closing.js -s FQ28 -m 2026-01 --bank FQ28-MN0001
7
+ //
8
+ // Hits BC OData/Beta API read-only. Writes a JSON snapshot to /tmp and
9
+ // prints a summary to stderr.
10
+
11
+ import { fileURLToPath } from 'url';
12
+ import { dirname, join } from 'path';
13
+ import { existsSync, writeFileSync } from 'fs';
14
+ import dotenv from 'dotenv';
15
+
16
+ const __dirname = dirname(fileURLToPath(import.meta.url));
17
+ const envPath = join(__dirname, '..', '.env');
18
+ if (existsSync(envPath)) dotenv.config({ path: envPath });
19
+
20
+ const { BCClient } = await import('../lib/bc-client.js');
21
+ const { handleReconciliationStatus } = await import('../tools/auditoria/reconciliation-status.js');
22
+ const { handleBankReconciliationReport } = await import('../tools/auditoria/bank-reconciliation-report.js');
23
+ const { handleOpenReceivables } = await import('../tools/cobranzas/open-receivables.js');
24
+ const { handleOpenPayables } = await import('../tools/cobranzas/open-payables.js');
25
+ const { resolveStores } = await import('../config/company-config.js');
26
+
27
+ // ── CLI args ───────────────────────────────────────────────
28
+ function parseArgs(argv) {
29
+ const out = {};
30
+ for (let i = 2; i < argv.length; i++) {
31
+ const a = argv[i];
32
+ const next = argv[i + 1];
33
+ if (a === '--store' || a === '-s') { out.store = next; i++; }
34
+ else if (a === '--month' || a === '-m') { out.month = next; i++; }
35
+ else if (a === '--bank' || a === '-b') { out.bank = next; i++; }
36
+ else if (a === '--help' || a === '-h') { out.help = true; }
37
+ }
38
+ return out;
39
+ }
40
+ const args = parseArgs(process.argv);
41
+ if (args.help || !args.store || !args.month) {
42
+ console.error('Usage: node scripts/diagnose-closing.js --store FQ01 --month 2025-12 [--bank FQ01-MN0001]');
43
+ process.exit(args.help ? 0 : 2);
44
+ }
45
+
46
+ const STORE = args.store;
47
+ const MONTH = args.month;
48
+ const MONTH_END = lastDayOfMonth(MONTH);
49
+ const BANK_FILTER = args.bank || null;
50
+
51
+ const bc = new BCClient();
52
+ const out = { store: STORE, month: MONTH, bank_filter: BANK_FILTER, generated_at: new Date().toISOString() };
53
+
54
+ console.error(`\n=== Diagnostic ${STORE} ${MONTH} ${BANK_FILTER ? '/' + BANK_FILTER : ''} ===\n`);
55
+
56
+ // 1. Reconciliation status
57
+ try {
58
+ console.error('[1/4] reconciliation_status…');
59
+ out.reconciliation_status = await handleReconciliationStatus(bc, { store: STORE });
60
+ const recs = out.reconciliation_status.reconciliations || [];
61
+ const monthRecs = recs.filter((r) => (r.statement_date || '').startsWith(MONTH));
62
+ console.error(` → ${recs.length} open reconciliations total; ${monthRecs.length} for ${MONTH}`);
63
+ for (const r of monthRecs) {
64
+ if (BANK_FILTER && r.bank_account_no !== BANK_FILTER) continue;
65
+ console.error(` ${r.bank_account_no} #${r.statement_no} date=${r.statement_date} matched=${r.matched_lines}/${r.total_lines} ending=${r.statement_ending_balance}`);
66
+ }
67
+ out.month_reconciliations = monthRecs;
68
+ } catch (err) {
69
+ console.error(' ✗', err.message);
70
+ out.reconciliation_status_error = err.message;
71
+ }
72
+
73
+ // 2. Bank reconciliation report (per bank or filter)
74
+ try {
75
+ console.error('\n[2/4] bank_reconciliation_report…');
76
+ const targets = BANK_FILTER
77
+ ? [BANK_FILTER]
78
+ : (out.month_reconciliations || []).map((r) => r.bank_account_no);
79
+ out.bank_reports = {};
80
+ for (const bank of targets) {
81
+ const r = await handleBankReconciliationReport(bc, {
82
+ store: STORE, bank_account: bank, month: MONTH, include_suggestions: true,
83
+ });
84
+ out.bank_reports[bank] = {
85
+ statement_no: r.statement_no,
86
+ statement_date: r.statement_date,
87
+ balance_last_statement: r.balance_last_statement,
88
+ statement_ending_balance: r.statement_ending_balance,
89
+ unmatched_debits_count: r.unmatched_debits?.count,
90
+ unmatched_debits_total: r.unmatched_debits?.total_amount,
91
+ unmatched_credits_count: r.unmatched_credits?.count,
92
+ unmatched_credits_total: r.unmatched_credits?.total_amount,
93
+ match_rate_pct: r.unmatched_summary?.match_rate_pct,
94
+ suggestions_summary: r.journal_suggestions?.summary,
95
+ };
96
+ console.error(` ${bank}: end=${r.statement_ending_balance} unmD=${r.unmatched_debits?.count} unmC=${r.unmatched_credits?.count} match=${r.unmatched_summary?.match_rate_pct}%`);
97
+ }
98
+ } catch (err) {
99
+ console.error(' ✗', err.message);
100
+ out.bank_reports_error = err.message;
101
+ }
102
+
103
+ // 3. Open receivables / payables as of month_end
104
+ try {
105
+ console.error('\n[3/4] open_receivables + open_payables…');
106
+ const [ar, ap] = await Promise.all([
107
+ handleOpenReceivables(bc, { store: STORE, as_of_date: MONTH_END }),
108
+ handleOpenPayables(bc, { store: STORE, as_of_date: MONTH_END }),
109
+ ]);
110
+ out.ar_summary = { customers: ar.receivables_by_customer?.length, aging: ar.aging_summary };
111
+ out.ap_summary = { vendors: ap.payables_by_vendor?.length, aging: ap.aging_summary };
112
+ console.error(` AR: ${ar.receivables_by_customer?.length} customers, total ${ar.aging_summary?.total} USD`);
113
+ console.error(` AP: ${ap.payables_by_vendor?.length} vendors, total ${ap.aging_summary?.total} USD`);
114
+ } catch (err) {
115
+ console.error(' ✗', err.message);
116
+ out.arap_error = err.message;
117
+ }
118
+
119
+ // 4. Multi-Payment drafts (Gestion BC) — Transferred status
120
+ try {
121
+ console.error('\n[4/4] multi-payment drafts (Transferred)…');
122
+ const { handleDraftPayables, handleDraftReceivables } = await import('../tools/multi-payment/index.js');
123
+ const [dp, dr] = await Promise.all([
124
+ handleDraftPayables(bc, { store: STORE }),
125
+ handleDraftReceivables(bc, { store: STORE }),
126
+ ]);
127
+ // Count Transferred drafts
128
+ const tp = (dp?.drafts_by_status?.Transferred || dp?.results || []).length;
129
+ const tr = (dr?.drafts_by_status?.Transferred || dr?.results || []).length;
130
+ out.mp_drafts = { transferred_payables: tp, transferred_receivables: tr };
131
+ console.error(` Transferred drafts → payables: ${tp}, receivables: ${tr}`);
132
+ } catch (err) {
133
+ console.error(' ✗', err.message);
134
+ out.mp_error = err.message;
135
+ }
136
+
137
+ const outPath = `/tmp/diagnose-${STORE}-${MONTH}.json`;
138
+ writeFileSync(outPath, JSON.stringify(out, null, 2));
139
+ console.error(`\n✓ Snapshot → ${outPath}\n`);
140
+
141
+ function lastDayOfMonth(yyyyMm) {
142
+ const [y, m] = yyyyMm.split('-').map(Number);
143
+ return new Date(Date.UTC(y, m, 0)).toISOString().slice(0, 10);
144
+ }