@fullqueso/mcp-bc-gastos 1.25.0 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## [1.26.0] — 2026-06-05
4
+
5
+ ### Added
6
+ - **`get_cash_flow` — posición de caja + efecto cambiario sobre la caja VES (Fase 2 FX).** Los bancos/caja en bolívares reciben depósitos que se dolarizan a la tasa del día, pero el VES se devalúa ~0.75–1%/día y BC **no postea revaluación** → el USD en libros sobrestima la caja real. Nuevo bloque `cash_position` que revalúa el saldo VES a la tasa de fin de período. **No afecta el FCF** (queda 100% operativo; el efecto cambiario es un memo de balance).
7
+ - **Fuente:** `generalLedgerEntries` trae ambas monedas por cuenta — `debitAmount/creditAmount` = USD (libro) y `additionalCurrencyDebitAmount/additionalCurrencyCreditAmount` = VES (saldo físico). Nuevo método `bcClient.getCashGLBalances(companyId, asOfDate)` (acumulado a fecha, cuentas 18xxx, ambas monedas).
8
+ - **Métrica:** por cada cuenta VES, `usd_real = ves_balance / rate_end`; `fx_adjustment = usd_real − usd_book` (< 0 cuando el VES se devaluó). `cash_position` expone `ves {book_usd, real_usd, fx_adjustment, accounts[]}`, `usd {total_usd}`, `total_cash_book_usd`, `total_cash_real_usd`, `rate_end`, `month_devaluation_pct`. Insight automático cuantificando la sobrestimación.
9
+ - **Regla VES (estructural, confirmada por FP):** bancos VES = `18200–18299`; caja VEB = `18110/18130/18160`; resto de 18xxx = USD (sin ajuste, tenencia física en USD). Captura todas las cuentas VES sin mapear banco→GL por cuenta.
10
+ - Nuevo param `include_cash_position` (default `true`). Card HTML "Posición de Caja · Efecto Cambiario (VES)" en single/multi/consolidado. Solo se calcula para el período actual (es saldo a fin de período).
11
+ - **Verificado contra BC real (FQ88, 31-may-2026):** caja VES libro **$34,873** → real **$32,583** (a 554.43 VES/USD) → ajuste **−$2,290** (6.57%); caja total libro $35,310 → real $33,020; devaluación del mes **13.25%**. La regla estructural capturó las 10 cuentas VES (2 más que el sondeo manual: 18275, 18297). FCF intacto en $30,165.08.
12
+
13
+ ---
14
+
3
15
  ## [1.25.0] — 2026-06-05
4
16
 
5
17
  ### Added
package/lib/bc-client.js CHANGED
@@ -316,6 +316,19 @@ export class BCClient {
316
316
  return this.apiCallAllPages(url);
317
317
  }
318
318
 
319
+ // Saldos acumulados (a fecha) de cuentas con AMBAS monedas: debit/creditAmount = USD (LCY)
320
+ // y additionalCurrency*Amount = VES (moneda adicional). Para revaluación de caja VES.
321
+ // postingDate le asOfDate (acumulado desde el inicio = saldo a la fecha).
322
+ async getCashGLBalances(companyId, asOfDate, accountMin = '18000', accountMax = '18999', maxPages = 60) {
323
+ const ed = this._sanitizeOData(asOfDate);
324
+ const url = this.buildApiUrl(companyId, 'generalLedgerEntries', {
325
+ $filter: `postingDate le ${ed} and accountNumber ge '${accountMin}' and accountNumber le '${accountMax}'`,
326
+ $select: 'accountNumber,description,debitAmount,creditAmount,additionalCurrencyDebitAmount,additionalCurrencyCreditAmount',
327
+ $orderby: 'postingDate desc',
328
+ });
329
+ return this.apiCallAllPages(url, maxPages);
330
+ }
331
+
319
332
  // Fetch revenue entries (40000-49999) - credit balance = income
320
333
  async getRevenueEntries(companyId, startDate, endDate) {
321
334
  return this.getGLEntries(companyId, startDate, endDate, '40000', '49999');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fullqueso/mcp-bc-gastos",
3
- "version": "1.25.0",
3
+ "version": "1.26.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": {
@@ -38,6 +38,7 @@ ${renderWaterfall(s.fcf_bridge)}
38
38
  ${renderKpis(s)}
39
39
  ${renderWorkingCapital(s)}
40
40
  ${renderCapexCard(s)}
41
+ ${renderCashPositionCard(s)}
41
42
  ${comparison ? renderComparisonCard({ [code]: comparison }, result) : ''}
42
43
  `;
43
44
  return renderShell(`${s.store_code} · Cash Flow · ${result.period.label}`, header, body, result);
@@ -67,6 +68,7 @@ function renderMulti(result) {
67
68
  ${renderWaterfall(s.fcf_bridge)}
68
69
  ${renderKpis(s)}
69
70
  ${renderWorkingCapital(s)}
71
+ ${renderCashPositionCard(s)}
70
72
  </div>
71
73
  </details>`;
72
74
  })
@@ -78,6 +80,7 @@ ${renderStoresComparison(result)}
78
80
  ${con ? renderWaterfall(con.fcf_bridge) : ''}
79
81
  ${con ? renderKpis(con) : ''}
80
82
  ${con ? renderWorkingCapital(con) : ''}
83
+ ${con ? renderCashPositionCard(con) : ''}
81
84
  ${result.comparison ? renderComparisonCard(result.comparison, result) : ''}
82
85
  <div class="section-label">Detalle por tienda</div>
83
86
  ${blocks}
@@ -268,6 +271,28 @@ function renderCapexCard(s) {
268
271
  </div>`;
269
272
  }
270
273
 
274
+ // Posición de caja + efecto cambiario sobre la caja VES (no afecta el FCF).
275
+ function renderCashPositionCard(s) {
276
+ const cp = s.cash_position;
277
+ if (!cp || !cp.ves || !cp.ves.accounts.length) return '';
278
+ const adj = cp.ves.fx_adjustment;
279
+ const dev = cp.month_devaluation_pct != null ? ` · devaluación mes ${cp.month_devaluation_pct}%` : '';
280
+ const acctRows = cp.ves.accounts
281
+ .map((a) => `<div class="pnl-row sub"><span class="pnl-label">${esc(a.account)} ${esc(a.name || '')}${a.store ? ` <span class="pnl-acct">${esc(a.store)}</span>` : ''}</span><span class="pnl-amt ${a.fx_adjustment < 0 ? 'val-red' : ''}">${signed(a.fx_adjustment)}</span></div>`)
282
+ .join('');
283
+ return `<div class="card">
284
+ <div class="card-hd"><span class="card-icon">&#x1F4B1;</span><span class="card-title">Posición de Caja · Efecto Cambiario (VES)</span></div>
285
+ <div class="card-bd"><div class="pnl">
286
+ <div class="pnl-row"><span class="pnl-label">Caja VES — libro (USD)</span><span class="pnl-amt">${signed(cp.ves.book_usd)}</span></div>
287
+ <div class="pnl-row"><span class="pnl-label">Caja VES — real (a ${cp.rate_end} VES/USD)</span><span class="pnl-amt">${signed(cp.ves.real_usd)}</span></div>
288
+ <div class="pnl-row gross"><span class="pnl-label">= Ajuste cambiario no contabilizado${dev}</span><span class="pnl-amt ${adj < 0 ? 'val-red' : 'val-green'}">${signed(adj)}</span></div>
289
+ <div class="pnl-section-hd">Por cuenta VES</div>
290
+ ${acctRows}
291
+ <div class="pnl-row total"><span class="pnl-label">Caja total — libro ${signed(cp.total_cash_book_usd)} → real</span><span class="pnl-amt">${signed(cp.total_cash_real_usd)}</span></div>
292
+ </div></div>
293
+ </div>`;
294
+ }
295
+
271
296
  // ─────────────────────────────────────────────────────────────────────────────
272
297
  // Comparativo de tiendas (multi)
273
298
  // ─────────────────────────────────────────────────────────────────────────────
@@ -53,7 +53,9 @@ export const cashFlowTool = {
53
53
  'Parte del EBIT (igual que get_financial_statements), resta impuesto estimado (ISLR 34%), ajusta por capital de trabajo ' +
54
54
  '(7 líneas desde net-change G/L: Δ cuentas por cobrar, Δ impuestos corrientes, Δ prepagados, Δ inventario, Δ cuentas por pagar, ' +
55
55
  'Δ impuestos por pagar, Δ pasivos laborales) para obtener el flujo de caja operativo (CFO), y resta CapEx (PP&E 11110-12240) ' +
56
- 'para el FCF. Reporta también D&A y EBITDA (EBIT + D&A). Incluye NOPAT, margen FCF, status ' +
56
+ 'para el FCF. Reporta también D&A y EBITDA (EBIT + D&A). Incluye además la posición de caja con efecto cambiario: revalúa ' +
57
+ 'el saldo de las cuentas de banco/caja en bolívares (VES) a la tasa de fin de período y muestra cuánto sobrestiman los libros ' +
58
+ 'la caja real por la devaluación no contabilizada (no afecta el FCF). Incluye NOPAT, margen FCF, status ' +
57
59
  '(healthy/positive_low/negative_watch/' +
58
60
  'negative_critical), bridge tipo waterfall e insights automáticos. Multi-tienda calcula consolidado. Con render_html=true ' +
59
61
  'genera un dashboard HTML (lo guarda en ~/Downloads y retorna la ruta). Úsalo cuando el usuario pida flujo de caja, ' +
@@ -81,6 +83,11 @@ export const cashFlowTool = {
81
83
  description: 'Si true, incluye comparación con el mes anterior (MoM). Default: false.',
82
84
  default: false,
83
85
  },
86
+ include_cash_position: {
87
+ type: 'boolean',
88
+ description: 'Si true (default), calcula la posición de caja y el efecto cambiario sobre la caja VES (revaluación del saldo en bolívares a la tasa de fin de período). No afecta el FCF.',
89
+ default: true,
90
+ },
84
91
  render_html: {
85
92
  type: 'boolean',
86
93
  description: 'Si true, genera el dashboard HTML, lo guarda en ~/Downloads y retorna la ruta. Default: false.',
@@ -112,12 +119,22 @@ export async function getCashFlow(bcClient, params = {}) {
112
119
 
113
120
  const compare = !!params.compare_previous;
114
121
  const prevRange = compare ? previousMonthRange(start, end) : null;
115
-
116
- // ── Fetch en paralelo: cash flow actual + previo por tienda + exchange rate ──
122
+ const includeCashPosition = params.include_cash_position !== false; // default true
123
+
124
+ // Tipo de cambio VES: serie completa (compartida); se obtiene una vez y se pasa a las
125
+ // tiendas (la posición de caja la usa para revaluar el saldo VES a la tasa de fin de mes).
126
+ const rates = await bcClient.getExchangeRates(stores[0].code).catch((err) => {
127
+ logger.warn(`[cash-flow] exchange rate fetch failed: ${err.message}`);
128
+ return null;
129
+ });
130
+ const fx = rates ? bcClient.getExchangeRateForDate(rates, end) : null;
131
+
132
+ // ── Fetch en paralelo: cash flow actual + previo por tienda ──
133
+ // La posición de caja (FX) solo se calcula para el período actual (es saldo a fin de período).
117
134
  const tasks = [];
118
135
  for (const s of stores) {
119
136
  tasks.push(
120
- buildStoreCashFlow(bcClient, s, start, end)
137
+ buildStoreCashFlow(bcClient, s, start, end, { rates, includeCashPosition })
121
138
  .then((cf) => ({ kind: 'current', code: s.code, cf }))
122
139
  .catch((err) => ({ kind: 'current', code: s.code, error: err.message, store: s }))
123
140
  );
@@ -125,24 +142,14 @@ export async function getCashFlow(bcClient, params = {}) {
125
142
  if (compare) {
126
143
  for (const s of stores) {
127
144
  tasks.push(
128
- buildStoreCashFlow(bcClient, s, prevRange.start, prevRange.end)
145
+ buildStoreCashFlow(bcClient, s, prevRange.start, prevRange.end, { rates, includeCashPosition: false })
129
146
  .then((cf) => ({ kind: 'previous', code: s.code, cf }))
130
147
  .catch((err) => ({ kind: 'previous', code: s.code, error: err.message }))
131
148
  );
132
149
  }
133
150
  }
134
151
 
135
- const fxTask = bcClient
136
- .getExchangeRates(stores[0].code)
137
- .then((rates) => bcClient.getExchangeRateForDate(rates, end))
138
- .catch((err) => {
139
- logger.warn(`[cash-flow] exchange rate fetch failed: ${err.message}`);
140
- return null;
141
- });
142
-
143
- const settled = await Promise.all([fxTask, ...tasks]);
144
- const fx = settled[0];
145
- const fetched = settled.slice(1);
152
+ const fetched = await Promise.all(tasks);
146
153
 
147
154
  // ── Partición current / previous ──
148
155
  const storesOut = {};
@@ -201,13 +208,16 @@ export async function getCashFlow(bcClient, params = {}) {
201
208
  // Cash flow de una tienda
202
209
  // ─────────────────────────────────────────────────────────────────────────────
203
210
 
204
- async function buildStoreCashFlow(bcClient, store, start, end) {
211
+ async function buildStoreCashFlow(bcClient, store, start, end, opts = {}) {
205
212
  logger.info(`[cash-flow] ${store.code}: ${start} → ${end}`);
206
213
 
207
- // P&L base (income/cogs/EBIT) en paralelo con working capital + capex
208
- const [{ financials }, wc] = await Promise.all([
214
+ // P&L base (income/cogs/EBIT) en paralelo con working capital + capex + posición de caja (FX)
215
+ const [{ financials }, wc, cashPosition] = await Promise.all([
209
216
  fetchStoreFinancials(bcClient, store, start, end),
210
217
  fetchWorkingCapitalAndCapex(bcClient, store.companyId, start, end),
218
+ opts.includeCashPosition && opts.rates
219
+ ? fetchCashPosition(bcClient, store.companyId, start, end, opts.rates)
220
+ : Promise.resolve(null),
211
221
  ]);
212
222
 
213
223
  return assembleCashFlow(
@@ -228,11 +238,87 @@ async function buildStoreCashFlow(bcClient, store, start, end) {
228
238
  delta_labor: wc.delta_labor,
229
239
  capex: wc.capex,
230
240
  capex_accounts: wc.capex_accounts,
241
+ cash_position: cashPosition,
231
242
  warnings: wc.warnings,
232
243
  }
233
244
  );
234
245
  }
235
246
 
247
+ // ─────────────────────────────────────────────────────────────────────────────
248
+ // Posición de caja + efecto cambiario (devaluación VES) — READ-ONLY, NO afecta el FCF.
249
+ // Regla VES (confirmada por FP, estructura compartida entre tiendas):
250
+ // bancos VES = 18200–18299 ; caja VEB = 18110/18130/18160. Resto de 18xxx = USD.
251
+ // El saldo VES vive en additionalCurrency (VES); el USD libro (debit/credit) quedó "stale"
252
+ // a tasas históricas. fx_adjustment = USD_real (VES/tasa_fin) − USD_libro (< 0 si VES se devaluó).
253
+ // ─────────────────────────────────────────────────────────────────────────────
254
+
255
+ const VES_CASH_ACCOUNTS = new Set(['18110', '18130', '18160']);
256
+ function isVesCashAccount(accountNumber) {
257
+ const n = parseInt(accountNumber, 10);
258
+ return (n >= 18200 && n <= 18299) || VES_CASH_ACCOUNTS.has(String(accountNumber));
259
+ }
260
+
261
+ async function fetchCashPosition(bcClient, companyId, start, end, rates) {
262
+ const entries = await bcClient.getCashGLBalances(companyId, end).catch((e) => {
263
+ logger.warn(`[cash-flow] cash balances fetch failed: ${e.message}`);
264
+ return null;
265
+ });
266
+ if (entries == null) return null;
267
+
268
+ const rateEnd = (bcClient.getExchangeRateForDate(rates, end) || {}).rate || null;
269
+ const rateStart = (bcClient.getExchangeRateForDate(rates, start) || {}).rate || null;
270
+ if (!rateEnd) return null;
271
+
272
+ const warnings = [];
273
+ if (entries._truncated) warnings.push('G/L de caja/bancos truncado — saldos pueden estar incompletos.');
274
+
275
+ // Acumular por cuenta: USD libro (debit−credit) y VES (additionalCurrency debit−credit)
276
+ // (no se usa e.description como nombre: es la nota de la transacción, no el nombre de la cuenta)
277
+ const acc = {};
278
+ for (const e of entries) {
279
+ const a = e.accountNumber;
280
+ if (!acc[a]) acc[a] = { usd: 0, ves: 0 };
281
+ acc[a].usd += (e.debitAmount || 0) - (e.creditAmount || 0);
282
+ acc[a].ves += (e.additionalCurrencyDebitAmount || 0) - (e.additionalCurrencyCreditAmount || 0);
283
+ }
284
+
285
+ const vesAccounts = [];
286
+ let vesBook = 0, vesReal = 0, usdTotal = 0;
287
+ for (const [a, v] of Object.entries(acc)) {
288
+ const usdBook = round2(v.usd);
289
+ if (Math.abs(usdBook) < 0.005 && Math.abs(v.ves) < 0.5) continue; // saldo cero
290
+ if (isVesCashAccount(a)) {
291
+ const usdReal = round2(v.ves / rateEnd);
292
+ vesBook += usdBook;
293
+ vesReal += usdReal;
294
+ const n = parseInt(a, 10);
295
+ vesAccounts.push({
296
+ account: a, name: n >= 18200 && n <= 18299 ? 'Banco VES' : 'Caja VES',
297
+ usd_book: usdBook, ves_balance: round2(v.ves), usd_real: usdReal,
298
+ fx_adjustment: round2(usdReal - usdBook),
299
+ });
300
+ } else {
301
+ usdTotal += usdBook; // cuentas USD: tenencia física en USD → sin ajuste
302
+ }
303
+ }
304
+ vesAccounts.sort((x, y) => x.fx_adjustment - y.fx_adjustment); // mayor pérdida primero
305
+
306
+ vesBook = round2(vesBook);
307
+ vesReal = round2(vesReal);
308
+ usdTotal = round2(usdTotal);
309
+
310
+ return {
311
+ rate_end: round2(rateEnd),
312
+ rate_start: rateStart ? round2(rateStart) : null,
313
+ month_devaluation_pct: rateStart && rateStart > 0 ? round2((rateEnd / rateStart - 1) * 100) : null,
314
+ ves: { book_usd: vesBook, real_usd: vesReal, fx_adjustment: round2(vesReal - vesBook), accounts: vesAccounts },
315
+ usd: { total_usd: usdTotal },
316
+ total_cash_book_usd: round2(vesBook + usdTotal),
317
+ total_cash_real_usd: round2(vesReal + usdTotal),
318
+ warnings,
319
+ };
320
+ }
321
+
236
322
  // Buckets del balance general (Chart of Accounts real de BC), por rango de cuenta.
237
323
  // REGLA UNIVERSAL de impacto en caja: delta = −Σ(debit − credit) sobre el rango.
238
324
  // - Activo que sube (debit) → −Σ < 0 = uso de caja.
@@ -359,7 +445,8 @@ function assembleCashFlow(meta, c) {
359
445
  const status = fcfStatus(fcf, fcfPct, ebit);
360
446
  const wc = { delta_ar, delta_taxes, delta_prepaid, delta_inventory, delta_ap, delta_taxes_payable, delta_labor };
361
447
  const bridge = buildBridge({ ebit, tax, nopat, ...wc, cfo, capex, fcf });
362
- const insights = buildInsights({ ebit, ...wc, da, capex, fcf, fcfPct, status, warnings: c.warnings || [] });
448
+ const cash_position = c.cash_position || null;
449
+ const insights = buildInsights({ ebit, ...wc, da, capex, fcf, fcfPct, status, cash_position, warnings: c.warnings || [] });
363
450
 
364
451
  return {
365
452
  store_code: meta.store_code,
@@ -376,6 +463,7 @@ function assembleCashFlow(meta, c) {
376
463
  cfo: { amount: cfo, pct: cfoPct },
377
464
  capex: { amount: capex, accounts: c.capex_accounts || {} },
378
465
  fcf: { amount: fcf, pct: fcfPct, status },
466
+ cash_position,
379
467
  fcf_bridge: bridge,
380
468
  insights,
381
469
  };
@@ -406,9 +494,23 @@ function buildBridge({ ebit, tax, nopat, delta_ar, delta_taxes, delta_prepaid, d
406
494
  ];
407
495
  }
408
496
 
409
- function buildInsights({ ebit, delta_ar, delta_taxes, delta_prepaid, delta_inventory, delta_ap, delta_taxes_payable, delta_labor, da, capex, fcf, fcfPct, status, warnings }) {
497
+ function buildInsights({ ebit, delta_ar, delta_taxes, delta_prepaid, delta_inventory, delta_ap, delta_taxes_payable, delta_labor, da, capex, fcf, fcfPct, status, cash_position, warnings }) {
410
498
  const insights = [];
411
499
 
500
+ // Efecto cambiario sobre caja VES — el saldo en libros sobrestima la caja real porque
501
+ // los bolívares no se han revaluado a la tasa de cierre (devaluación no contabilizada).
502
+ if (cash_position && cash_position.ves.book_usd > 0) {
503
+ const adj = cash_position.ves.fx_adjustment;
504
+ const pct = round2((adj / cash_position.ves.book_usd) * 100);
505
+ if (adj < 0 && Math.abs(adj) >= 1) {
506
+ const dev = cash_position.month_devaluation_pct != null ? ` (devaluación del mes ${cash_position.month_devaluation_pct}%)` : '';
507
+ insights.push(
508
+ `Caja VES sobrestimada $${fmtAbs(adj)} (${Math.abs(pct)}%) por devaluación no contabilizada — ` +
509
+ `valor real $${fmtAbs(cash_position.total_cash_real_usd)} vs $${fmtAbs(cash_position.total_cash_book_usd)} en libros${dev}.`
510
+ );
511
+ }
512
+ }
513
+
412
514
  // Mayor driver negativo entre los usos de capital de trabajo (7 líneas)
413
515
  const uses = [
414
516
  { key: 'delta_inventory', label: 'Inventario', val: delta_inventory },
@@ -453,6 +555,7 @@ function buildInsights({ ebit, delta_ar, delta_taxes, delta_prepaid, delta_inven
453
555
  }
454
556
 
455
557
  insights.push(...warnings);
558
+ if (cash_position && cash_position.warnings) insights.push(...cash_position.warnings);
456
559
 
457
560
  if (insights.length === 0) {
458
561
  insights.push('Flujo de caja dentro de rangos normales.');
@@ -494,11 +597,39 @@ function consolidateCashFlow(storeObjs) {
494
597
  delta_labor: sum((o) => o.working_capital.delta_labor || 0),
495
598
  capex: sum((o) => o.capex.amount),
496
599
  capex_accounts,
600
+ cash_position: consolidateCashPosition(storeObjs),
497
601
  warnings: allWarnings,
498
602
  }
499
603
  );
500
604
  }
501
605
 
606
+ // Consolida la posición de caja (FX) sumando escalares y concatenando cuentas VES.
607
+ function consolidateCashPosition(storeObjs) {
608
+ const positions = storeObjs.map((o) => o.cash_position).filter(Boolean);
609
+ if (!positions.length) return null;
610
+ const sumP = (fn) => round2(positions.reduce((s, p) => s + fn(p), 0));
611
+ const accounts = [];
612
+ for (const o of storeObjs) {
613
+ if (!o.cash_position) continue;
614
+ for (const a of o.cash_position.ves.accounts) accounts.push({ ...a, store: o.store_code });
615
+ }
616
+ accounts.sort((x, y) => x.fx_adjustment - y.fx_adjustment);
617
+ const vesBook = sumP((p) => p.ves.book_usd);
618
+ const vesReal = sumP((p) => p.ves.real_usd);
619
+ const usdTotal = sumP((p) => p.usd.total_usd);
620
+ const ref = positions[0]; // tasa VES compartida entre tiendas
621
+ return {
622
+ rate_end: ref.rate_end,
623
+ rate_start: ref.rate_start,
624
+ month_devaluation_pct: ref.month_devaluation_pct,
625
+ ves: { book_usd: vesBook, real_usd: vesReal, fx_adjustment: round2(vesReal - vesBook), accounts },
626
+ usd: { total_usd: usdTotal },
627
+ total_cash_book_usd: round2(vesBook + usdTotal),
628
+ total_cash_real_usd: round2(vesReal + usdTotal),
629
+ warnings: [],
630
+ };
631
+ }
632
+
502
633
  // ─────────────────────────────────────────────────────────────────────────────
503
634
  // Comparación MoM (compacta)
504
635
  // ─────────────────────────────────────────────────────────────────────────────