@frihet/mcp-server 1.17.0 → 2.0.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 (87) hide show
  1. package/README.md +84 -59
  2. package/dist/agent-onboarding.d.ts +1 -1
  3. package/dist/agent-onboarding.js +3 -3
  4. package/dist/canary/mcp-harness.d.ts +1 -1
  5. package/dist/canary/mcp-harness.js +4 -1
  6. package/dist/capability-truth.js +20 -12
  7. package/dist/claude-candidate-contract.d.ts +482 -0
  8. package/dist/claude-candidate-contract.js +87 -0
  9. package/dist/claude-output-privacy.d.ts +17 -0
  10. package/dist/claude-output-privacy.js +240 -0
  11. package/dist/claude-profile.d.ts +18 -0
  12. package/dist/claude-profile.js +260 -0
  13. package/dist/client-interface.d.ts +26 -20
  14. package/dist/client.d.ts +65 -29
  15. package/dist/client.js +278 -167
  16. package/dist/demo-client.d.ts +27 -21
  17. package/dist/demo-client.js +31 -14
  18. package/dist/deposit-contract.d.ts +29 -0
  19. package/dist/deposit-contract.js +76 -0
  20. package/dist/fetch-no-redirect.d.ts +3 -0
  21. package/dist/fetch-no-redirect.js +16 -0
  22. package/dist/fiscal-aliases.d.ts +2 -2
  23. package/dist/fiscal-aliases.js +2 -2
  24. package/dist/fiscal-period.d.ts +26 -0
  25. package/dist/fiscal-period.js +31 -0
  26. package/dist/google-profile.d.ts +19 -0
  27. package/dist/google-profile.js +122 -0
  28. package/dist/index.js +31 -7
  29. package/dist/logger.d.ts +1 -1
  30. package/dist/logger.js +2 -2
  31. package/dist/muse-profile.d.ts +19 -0
  32. package/dist/muse-profile.js +122 -0
  33. package/dist/observability.js +3 -2
  34. package/dist/openai-profile.d.ts +2 -2
  35. package/dist/openai-profile.js +37 -11
  36. package/dist/openai-review-contract.d.ts +1 -1
  37. package/dist/openai-review-contract.js +4 -5
  38. package/dist/openai-review-oauth.d.ts +107 -7
  39. package/dist/openai-review-oauth.js +161 -17
  40. package/dist/prompts/register-all.js +32 -78
  41. package/dist/redaction.d.ts +5 -0
  42. package/dist/redaction.js +9 -0
  43. package/dist/resources/register-all.js +150 -54
  44. package/dist/retry-after.d.ts +19 -0
  45. package/dist/retry-after.js +84 -0
  46. package/dist/server-composition.d.ts +5 -2
  47. package/dist/server-composition.js +35 -5
  48. package/dist/tool-exposure.d.ts +6 -5
  49. package/dist/tool-exposure.js +15 -13
  50. package/dist/tools/audit_gl.js +8 -8
  51. package/dist/tools/backend-availability.d.ts +8 -29
  52. package/dist/tools/backend-availability.js +31 -30
  53. package/dist/tools/bank_rules.js +66 -15
  54. package/dist/tools/banking.js +5 -13
  55. package/dist/tools/clients.js +252 -6
  56. package/dist/tools/deposits.js +107 -97
  57. package/dist/tools/einvoice.d.ts +17 -3
  58. package/dist/tools/einvoice.js +59 -12
  59. package/dist/tools/fiscal.d.ts +3 -4
  60. package/dist/tools/fiscal.js +93 -60
  61. package/dist/tools/gestoria.js +15 -7
  62. package/dist/tools/hr.js +41 -11
  63. package/dist/tools/igic.d.ts +9 -8
  64. package/dist/tools/igic.js +34 -64
  65. package/dist/tools/impuesto_sociedades.d.ts +4 -5
  66. package/dist/tools/impuesto_sociedades.js +16 -37
  67. package/dist/tools/intelligence.js +164 -10
  68. package/dist/tools/invoices.js +109 -42
  69. package/dist/tools/kitchen.js +19 -9
  70. package/dist/tools/quotes.js +86 -25
  71. package/dist/tools/recurring.js +26 -25
  72. package/dist/tools/register-all.d.ts +2 -2
  73. package/dist/tools/register-all.js +9 -6
  74. package/dist/tools/search.d.ts +19 -0
  75. package/dist/tools/search.js +84 -0
  76. package/dist/tools/shared.d.ts +17 -37
  77. package/dist/tools/shared.js +96 -30
  78. package/dist/tools/team.js +8 -16
  79. package/dist/tools/time.js +4 -12
  80. package/dist/tools/vendors.js +240 -6
  81. package/dist/types.d.ts +3 -1
  82. package/dist/workflows/daily-operational-brief.d.ts +163 -0
  83. package/dist/workflows/daily-operational-brief.js +881 -0
  84. package/docs/agent-onboarding.json +15 -5
  85. package/package.json +26 -12
  86. package/dist/tools/onboarding.d.ts +0 -19
  87. package/dist/tools/onboarding.js +0 -64
@@ -0,0 +1,881 @@
1
+ /**
2
+ * Daily Operational Brief workflow ("¿Qué debería revisar hoy en mi negocio y por qué?").
3
+ *
4
+ * Grounded strictly in read-only Frihet workspace tools:
5
+ * - get_business_context
6
+ * - list_invoices
7
+ * - list_quotes
8
+ * - list_expenses
9
+ * - list_clients
10
+ * - list_client_activities
11
+ *
12
+ * Enforces:
13
+ * 1. Explicit temporal scope (`unfiltered_operational_sample_as_of_date` vs `currentMonthActivity`)
14
+ * and per-resource coverage (inspected vs total records).
15
+ * 2. Strict separation between draft documents (non-issued) and real/issued activity.
16
+ * 3. Explicit amount provenance (`stored_field`, `derived_from_items`, `business_context_summary`,
17
+ * `unavailable`, or `mixed_currency_unconverted`).
18
+ * - Absent/corrupt amounts NEVER coerce to 0 (`amount: null`, `provenance: "unavailable"`).
19
+ * - Multi-currency totals NEVER sum across currencies without conversion (`totalsByCurrency`, `currency: "MIXED"`).
20
+ * - Derived line-item totals apply canonical per-line `discount`, document `discountRate`,
21
+ * `taxRate`, `equivalenceSurchargeRate`, and `irpfRate` over the discounted tax-exclusive base.
22
+ * 4. Honest unpaid invoice semantics: full nominal totals of unpaid invoices are labeled
23
+ * "importe nominal de facturas no marcadas como pagadas; saldo pendiente real no determinable
24
+ * sin desglose de cobros parciales ni conciliación bancaria" (`realOutstandingBalanceDeterminable: false`).
25
+ * 5. Record-backed prioritized review items and suggested next actions without auto-executing writes.
26
+ * 6. Explicit declaration of what cannot be determined ("knownUnknowns").
27
+ */
28
+ function roundCurrency(value) {
29
+ return Math.round(value * 100) / 100;
30
+ }
31
+ function isFiniteRateInRange(value, min = 0, max = 100) {
32
+ if (value === undefined || value === null)
33
+ return true;
34
+ return typeof value === "number" && Number.isFinite(value) && value >= min && value <= max;
35
+ }
36
+ export function formatProvenancedAmount(p) {
37
+ if (p.provenance === "mixed_currency_unconverted" && p.totalsByCurrency) {
38
+ const parts = Object.values(p.totalsByCurrency).map((bucket) => `${bucket.amount.toFixed(2)} ${bucket.currency}`);
39
+ return `MIXED (${parts.join(" + ")})`;
40
+ }
41
+ if (p.amount === null || p.provenance === "unavailable") {
42
+ const unavailableCount = p.unavailableCount ?? 1;
43
+ const knownParts = p.totalsByCurrency
44
+ ? Object.values(p.totalsByCurrency).map((b) => `${b.amount.toFixed(2)} ${b.currency}`)
45
+ : [];
46
+ if (knownParts.length > 0) {
47
+ return `NO DETERMINADO (${unavailableCount} doc(s) sin importe válido; parcial conocido: ${knownParts.join(" + ")})`;
48
+ }
49
+ return `NO DETERMINADO (${unavailableCount} doc(s) sin importe válido)`;
50
+ }
51
+ return `${p.amount.toFixed(2)} ${p.currency}`;
52
+ }
53
+ export function resolveDocumentAmount(doc, defaultCurrency = "EUR") {
54
+ const currency = typeof doc.currency === "string" && doc.currency.trim()
55
+ ? doc.currency.trim().toUpperCase()
56
+ : defaultCurrency.trim().toUpperCase();
57
+ if (typeof doc.total === "number" && Number.isFinite(doc.total)) {
58
+ return {
59
+ amount: roundCurrency(doc.total),
60
+ currency,
61
+ provenance: "stored_field",
62
+ fieldOrFormula: "doc.total",
63
+ };
64
+ }
65
+ if (typeof doc.amount === "number" && Number.isFinite(doc.amount)) {
66
+ return {
67
+ amount: roundCurrency(doc.amount),
68
+ currency,
69
+ provenance: "stored_field",
70
+ fieldOrFormula: "doc.amount",
71
+ };
72
+ }
73
+ // Absent != 0: if neither stored total nor stored amount exists, we require a non-empty
74
+ // items array where every line item has finite non-negative quantity and unitPrice, and no
75
+ // unsupported calculation modifiers are present.
76
+ if (!Array.isArray(doc.items) || doc.items.length === 0) {
77
+ return {
78
+ amount: null,
79
+ currency,
80
+ provenance: "unavailable",
81
+ fieldOrFormula: "unavailable (missing total, amount, and valid line items)",
82
+ unavailableCount: 1,
83
+ };
84
+ }
85
+ // Reject unsupported or invalid document-level calculation modifiers when stored total is missing
86
+ if (doc.pricesIncludeTax === true ||
87
+ doc.deductLineDiscountAmounts === true ||
88
+ (Array.isArray(doc.compoundTaxes) && doc.compoundTaxes.length > 0) ||
89
+ (doc.prepayment !== undefined && doc.prepayment !== null && doc.prepayment !== 0) ||
90
+ !isFiniteRateInRange(doc.discountRate, 0, 100) ||
91
+ !isFiniteRateInRange(doc.taxRate, 0, 100) ||
92
+ !isFiniteRateInRange(doc.irpfRate, 0, 100) ||
93
+ !isFiniteRateInRange(doc.equivalenceSurchargeRate, 0, 100)) {
94
+ return {
95
+ amount: null,
96
+ currency,
97
+ provenance: "unavailable",
98
+ fieldOrFormula: "unavailable (unsupported or invalid calculation modifiers without stored total)",
99
+ unavailableCount: 1,
100
+ };
101
+ }
102
+ const items = doc.items;
103
+ const docTaxRate = typeof doc.taxRate === "number" ? doc.taxRate : 0;
104
+ const docDiscountPct = typeof doc.discountRate === "number" ? doc.discountRate : 0;
105
+ const docIrpfRate = typeof doc.irpfRate === "number" ? doc.irpfRate : 0;
106
+ const docSurchargeRate = typeof doc.equivalenceSurchargeRate === "number" ? doc.equivalenceSurchargeRate : 0;
107
+ const baseByRate = new Map();
108
+ let subtotalBeforeDiscount = 0;
109
+ for (const item of items) {
110
+ if (!item ||
111
+ typeof item !== "object" ||
112
+ typeof item.quantity !== "number" ||
113
+ !Number.isFinite(item.quantity) ||
114
+ item.quantity < 0 ||
115
+ typeof item.unitPrice !== "number" ||
116
+ !Number.isFinite(item.unitPrice) ||
117
+ item.unitPrice < 0 ||
118
+ (item.discountAmount !== undefined && item.discountAmount !== null && item.discountAmount !== 0) ||
119
+ !isFiniteRateInRange(item.discount, 0, 100) ||
120
+ !isFiniteRateInRange(item.taxRate, 0, 100)) {
121
+ return {
122
+ amount: null,
123
+ currency,
124
+ provenance: "unavailable",
125
+ fieldOrFormula: "unavailable (corrupt or incomplete line item without stored total)",
126
+ unavailableCount: 1,
127
+ };
128
+ }
129
+ const effectiveRate = typeof item.taxRate === "number" ? item.taxRate : docTaxRate;
130
+ const lineDiscountPct = typeof item.discount === "number" ? item.discount : 0;
131
+ const effectiveUnitPrice = roundCurrency(item.unitPrice * (1 - lineDiscountPct / 100));
132
+ const lineBase = roundCurrency(item.quantity * effectiveUnitPrice);
133
+ subtotalBeforeDiscount = roundCurrency(subtotalBeforeDiscount + lineBase);
134
+ baseByRate.set(effectiveRate, roundCurrency((baseByRate.get(effectiveRate) ?? 0) + lineBase));
135
+ }
136
+ const documentDiscount = roundCurrency(subtotalBeforeDiscount * (docDiscountPct / 100));
137
+ const subtotal = roundCurrency(subtotalBeforeDiscount - documentDiscount);
138
+ if (docDiscountPct > 0 && subtotalBeforeDiscount > 0) {
139
+ const rates = [...baseByRate.keys()].sort((a, b) => b - a);
140
+ for (let i = 0; i < rates.length; i++) {
141
+ const rate = rates[i];
142
+ const originalBase = baseByRate.get(rate);
143
+ if (i === rates.length - 1) {
144
+ let otherGroupsTotal = 0;
145
+ for (let j = 0; j < i; j++) {
146
+ otherGroupsTotal = roundCurrency(otherGroupsTotal + baseByRate.get(rates[j]));
147
+ }
148
+ baseByRate.set(rate, roundCurrency(subtotal - otherGroupsTotal));
149
+ }
150
+ else {
151
+ const groupDiscount = roundCurrency(originalBase * (docDiscountPct / 100));
152
+ baseByRate.set(rate, roundCurrency(originalBase - groupDiscount));
153
+ }
154
+ }
155
+ }
156
+ let totalTax = 0;
157
+ for (const [rate, base] of [...baseByRate.entries()].sort((a, b) => b[0] - a[0])) {
158
+ const tax = roundCurrency(base * (rate / 100));
159
+ totalTax = roundCurrency(totalTax + tax);
160
+ }
161
+ const irpf = roundCurrency(subtotal * (docIrpfRate / 100));
162
+ const surcharge = roundCurrency(subtotal * (docSurchargeRate / 100));
163
+ const total = roundCurrency(subtotal + totalTax + surcharge - irpf);
164
+ return {
165
+ amount: total,
166
+ currency,
167
+ provenance: "derived_from_items",
168
+ fieldOrFormula: "canonical_invoice_calculation(items[qty*round(unitPrice*(1-lineDiscount))], docDiscountRate, taxRate, equivalenceSurchargeRate, irpfRate)",
169
+ };
170
+ }
171
+ export function sumProvenancedAmounts(amounts, defaultCurrency = "EUR") {
172
+ const normalizedDefault = defaultCurrency.trim().toUpperCase();
173
+ if (amounts.length === 0) {
174
+ return {
175
+ amount: 0,
176
+ currency: normalizedDefault,
177
+ provenance: "stored_field",
178
+ fieldOrFormula: "sum(0 documents)",
179
+ totalsByCurrency: {},
180
+ unavailableCount: 0,
181
+ };
182
+ }
183
+ const totalsByCurrency = {};
184
+ let unavailableCount = 0;
185
+ for (const item of amounts) {
186
+ if (item.amount === null ||
187
+ !Number.isFinite(item.amount) ||
188
+ item.provenance === "unavailable") {
189
+ unavailableCount += item.unavailableCount ?? 1;
190
+ continue;
191
+ }
192
+ const curr = (item.currency || normalizedDefault).trim().toUpperCase();
193
+ const existing = totalsByCurrency[curr];
194
+ const itemProv = item.provenance === "derived_from_items" ? "derived_from_items" : "stored_field";
195
+ if (!existing) {
196
+ totalsByCurrency[curr] = {
197
+ amount: roundCurrency(item.amount),
198
+ currency: curr,
199
+ provenance: itemProv,
200
+ documentCount: 1,
201
+ };
202
+ }
203
+ else {
204
+ existing.amount = roundCurrency(existing.amount + item.amount);
205
+ existing.documentCount += 1;
206
+ if (itemProv === "derived_from_items") {
207
+ existing.provenance = "derived_from_items";
208
+ }
209
+ }
210
+ }
211
+ const currencies = Object.keys(totalsByCurrency);
212
+ if (unavailableCount > 0) {
213
+ return {
214
+ amount: null,
215
+ currency: currencies.length === 1 ? currencies[0] : currencies.length > 1 ? "MIXED" : normalizedDefault,
216
+ provenance: "unavailable",
217
+ fieldOrFormula: `unavailable (${unavailableCount} document(s) missing valid amount; never coerced to 0)`,
218
+ totalsByCurrency,
219
+ unavailableCount,
220
+ };
221
+ }
222
+ if (currencies.length > 1) {
223
+ const breakdownText = currencies
224
+ .sort()
225
+ .map((c) => `${c}: ${totalsByCurrency[c].amount.toFixed(2)}`)
226
+ .join(", ");
227
+ return {
228
+ amount: null,
229
+ currency: "MIXED",
230
+ provenance: "mixed_currency_unconverted",
231
+ fieldOrFormula: `unconverted multi-currency totals (${breakdownText})`,
232
+ totalsByCurrency,
233
+ unavailableCount: 0,
234
+ };
235
+ }
236
+ const singleCurr = currencies[0] ?? normalizedDefault;
237
+ const bucket = totalsByCurrency[singleCurr];
238
+ const anyDerived = bucket?.provenance === "derived_from_items";
239
+ return {
240
+ amount: bucket ? bucket.amount : 0,
241
+ currency: singleCurr,
242
+ provenance: anyDerived ? "derived_from_items" : "stored_field",
243
+ fieldOrFormula: anyDerived
244
+ ? "sum(stored doc.total/amount + canonical derived item totals)"
245
+ : "sum(stored doc.total/amount)",
246
+ totalsByCurrency,
247
+ unavailableCount: 0,
248
+ };
249
+ }
250
+ export async function generateDailyOperationalBrief(client, options) {
251
+ const asOfDate = options?.asOfDate ?? new Date().toISOString().slice(0, 10);
252
+ const periodMonth = asOfDate.slice(0, 7);
253
+ const pageLimit = options?.pageLimit ?? 20;
254
+ const maxActivityClients = options?.maxActivityClients ?? 5;
255
+ const [businessContext, invoicesPage, quotesPage, expensesPage, clientsPage] = await Promise.all([
256
+ client.getBusinessContext(),
257
+ client.listInvoices({ limit: pageLimit }),
258
+ client.listQuotes({ limit: pageLimit }),
259
+ client.listExpenses({ limit: pageLimit }),
260
+ client.listClients({ limit: pageLimit }),
261
+ ]);
262
+ const defaultCurrency = typeof businessContext?.company?.currency === "string"
263
+ ? String(businessContext.company.currency)
264
+ : "EUR";
265
+ const clientsToInspect = clientsPage.data.slice(0, maxActivityClients);
266
+ const activitiesByClient = await Promise.all(clientsToInspect.map(async (c) => {
267
+ const clientId = String(c.id ?? "");
268
+ const res = await client.listClientActivities(clientId, { limit: 10 });
269
+ return {
270
+ client: c,
271
+ activities: res.data,
272
+ total: res.total,
273
+ };
274
+ }));
275
+ const invoicesCoverage = {
276
+ collection: "invoices",
277
+ sourceTool: "list_invoices",
278
+ inspected: invoicesPage.data.length,
279
+ total: invoicesPage.total,
280
+ complete: invoicesPage.data.length >= invoicesPage.total,
281
+ };
282
+ const quotesCoverage = {
283
+ collection: "quotes",
284
+ sourceTool: "list_quotes",
285
+ inspected: quotesPage.data.length,
286
+ total: quotesPage.total,
287
+ complete: quotesPage.data.length >= quotesPage.total,
288
+ };
289
+ const expensesCoverage = {
290
+ collection: "expenses",
291
+ sourceTool: "list_expenses",
292
+ inspected: expensesPage.data.length,
293
+ total: expensesPage.total,
294
+ complete: expensesPage.data.length >= expensesPage.total,
295
+ };
296
+ const clientsCoverage = {
297
+ collection: "clients",
298
+ sourceTool: "list_clients",
299
+ inspected: clientsPage.data.length,
300
+ total: clientsPage.total,
301
+ complete: clientsPage.data.length >= clientsPage.total,
302
+ };
303
+ const clientActivitiesCoverage = {
304
+ collection: "client_activities",
305
+ sourceTool: "list_client_activities",
306
+ inspected: clientsToInspect.length,
307
+ total: clientsPage.total,
308
+ complete: clientsToInspect.length >= clientsPage.total &&
309
+ activitiesByClient.every((entry) => entry.activities.length >= entry.total),
310
+ };
311
+ const isPartial = !invoicesCoverage.complete ||
312
+ !quotesCoverage.complete ||
313
+ !expensesCoverage.complete ||
314
+ !clientsCoverage.complete ||
315
+ !clientActivitiesCoverage.complete;
316
+ const coverage = {
317
+ asOfDate,
318
+ periodMonth,
319
+ referenceMonth: periodMonth,
320
+ temporalScope: "unfiltered_operational_sample_as_of_date",
321
+ temporalScopeNote: `Las consultas list_invoices, list_quotes y list_expenses leen los registros recientes/abiertos a fecha de corte (${asOfDate}) sin filtro mensual para no ocultar impagos vencidos ni borradores de meses anteriores. La actividad específica del mes de referencia (${periodMonth}) se desglosa por separado en currentMonthActivity.`,
322
+ isPartial,
323
+ resources: {
324
+ invoices: invoicesCoverage,
325
+ quotes: quotesCoverage,
326
+ expenses: expensesCoverage,
327
+ clients: clientsCoverage,
328
+ client_activities: clientActivitiesCoverage,
329
+ },
330
+ };
331
+ // Separate draft invoices vs issued invoices (and inside issued: paid vs unpaid nominal [overdue vs on-time])
332
+ const invoiceDrafts = [];
333
+ const issuedInvoices = [];
334
+ const paidInvoices = [];
335
+ const outstandingInvoices = [];
336
+ const issuedInvoicesByStatus = {};
337
+ for (const rawInv of invoicesPage.data) {
338
+ const status = typeof rawInv.status === "string" ? rawInv.status : "draft";
339
+ const summary = {
340
+ id: String(rawInv.id ?? ""),
341
+ documentNumber: typeof rawInv.documentNumber === "string" && rawInv.documentNumber ? rawInv.documentNumber : null,
342
+ status,
343
+ counterpartyName: typeof rawInv.clientName === "string" && rawInv.clientName ? rawInv.clientName : "Cliente sin nombre",
344
+ issueDate: typeof rawInv.issueDate === "string" ? rawInv.issueDate : null,
345
+ dueDate: typeof rawInv.dueDate === "string" ? rawInv.dueDate : null,
346
+ amount: resolveDocumentAmount(rawInv, defaultCurrency),
347
+ };
348
+ if (status === "draft") {
349
+ invoiceDrafts.push(summary);
350
+ }
351
+ else if (status !== "cancelled") {
352
+ issuedInvoices.push(summary);
353
+ issuedInvoicesByStatus[status] = (issuedInvoicesByStatus[status] ?? 0) + 1;
354
+ if (status === "paid") {
355
+ paidInvoices.push(summary);
356
+ }
357
+ else {
358
+ outstandingInvoices.push(summary);
359
+ }
360
+ }
361
+ }
362
+ const quoteDrafts = [];
363
+ for (const rawQuote of quotesPage.data) {
364
+ const status = typeof rawQuote.status === "string" ? rawQuote.status : "draft";
365
+ if (status === "draft" || status === "sent") {
366
+ quoteDrafts.push({
367
+ id: String(rawQuote.id ?? ""),
368
+ documentNumber: typeof rawQuote.documentNumber === "string" && rawQuote.documentNumber ? rawQuote.documentNumber : null,
369
+ status,
370
+ counterpartyName: typeof rawQuote.clientName === "string" && rawQuote.clientName ? rawQuote.clientName : "Cliente sin nombre",
371
+ issueDate: typeof rawQuote.issueDate === "string" ? rawQuote.issueDate : null,
372
+ dueDate: typeof rawQuote.validUntil === "string"
373
+ ? rawQuote.validUntil
374
+ : typeof rawQuote.dueDate === "string"
375
+ ? rawQuote.dueDate
376
+ : null,
377
+ amount: resolveDocumentAmount(rawQuote, defaultCurrency),
378
+ });
379
+ }
380
+ }
381
+ const expenseAmounts = expensesPage.data.map((exp) => resolveDocumentAmount(exp, defaultCurrency));
382
+ const deductibleExpenses = expensesPage.data.filter((exp) => exp.taxDeductible === true);
383
+ const nonDeductibleExpenses = expensesPage.data.filter((exp) => exp.taxDeductible === false);
384
+ const expensesNeedingReview = expensesPage.data.filter((exp) => {
385
+ const missingVendor = typeof exp.vendor !== "string" || !exp.vendor.trim();
386
+ const missingCategory = typeof exp.category !== "string" || !exp.category.trim();
387
+ const unsetDeductibility = typeof exp.taxDeductible !== "boolean";
388
+ return missingVendor || missingCategory || unsetDeductibility;
389
+ });
390
+ // Current month (`periodMonth`) subset explicitly filtered by document date prefix
391
+ const issuedInvoicesInMonth = issuedInvoices.filter((inv) => typeof inv.issueDate === "string" && inv.issueDate.startsWith(periodMonth));
392
+ const invoiceDraftsInMonth = invoiceDrafts.filter((inv) => typeof inv.issueDate === "string" && inv.issueDate.startsWith(periodMonth));
393
+ const expensesInMonth = expensesPage.data.filter((exp) => typeof exp.date === "string" && String(exp.date).startsWith(periodMonth));
394
+ const priorities = [];
395
+ // 1. Overdue or past-due unpaid invoices (high priority)
396
+ const overdueInvoices = outstandingInvoices.filter((inv) => inv.status === "overdue" || (inv.dueDate !== null && inv.dueDate < asOfDate));
397
+ const onTimePendingInvoices = outstandingInvoices.filter((inv) => inv.status !== "overdue" && (inv.dueDate === null || inv.dueDate >= asOfDate));
398
+ if (overdueInvoices.length > 0) {
399
+ const overdueTotal = sumProvenancedAmounts(overdueInvoices.map((i) => i.amount), defaultCurrency);
400
+ priorities.push({
401
+ priority: "high",
402
+ category: "overdue_invoice",
403
+ title: `${overdueInvoices.length} factura(s) emitida(s) vencida(s) o fuera de plazo`,
404
+ why: `Las facturas emitidas (${overdueInvoices.map((i) => `${i.documentNumber ?? i.id} · ${i.counterpartyName}`).join(", ")}) tienen vencimiento anterior a ${asOfDate} o estado 'overdue' (importe nominal total de documentos no marcados como pagados; saldo pendiente real no determinado sin cobros parciales).`,
405
+ recordRefs: overdueInvoices.map((i) => ({
406
+ type: "invoice",
407
+ id: i.id,
408
+ label: `${i.documentNumber ?? i.id} (${i.counterpartyName})`,
409
+ })),
410
+ amount: overdueTotal,
411
+ suggestedNextAction: {
412
+ description: "Inspeccionar el detalle con get_invoice y registrar seguimiento comercial con log_client_activity (previa confirmación).",
413
+ recommendedTool: "get_invoice",
414
+ requiresConfirmation: false,
415
+ autoExecuted: false,
416
+ },
417
+ });
418
+ }
419
+ // 2. Draft invoices pending review (high/medium priority)
420
+ if (invoiceDrafts.length > 0) {
421
+ const draftsTotal = sumProvenancedAmounts(invoiceDrafts.map((i) => i.amount), defaultCurrency);
422
+ priorities.push({
423
+ priority: "high",
424
+ category: "draft_invoice",
425
+ title: `${invoiceDrafts.length} borrador(es) de factura pendientes de revisión`,
426
+ why: `Existen ${invoiceDrafts.length} borradores (${invoiceDrafts.map((i) => `${i.documentNumber ?? i.id} · ${i.counterpartyName}`).join(", ")}) que ya reservaron numeración pero siguen en estado 'draft' (no constituyen facturación emitida ni enviada).`,
427
+ recordRefs: invoiceDrafts.map((i) => ({
428
+ type: "invoice",
429
+ id: i.id,
430
+ label: `${i.documentNumber ?? i.id} (${i.counterpartyName})`,
431
+ })),
432
+ amount: draftsTotal,
433
+ suggestedNextAction: {
434
+ description: "Revisar líneas e importes de cada borrador con get_invoice antes de decidir su emisión en la aplicación web de Frihet.",
435
+ recommendedTool: "get_invoice",
436
+ requiresConfirmation: false,
437
+ autoExecuted: false,
438
+ },
439
+ });
440
+ }
441
+ // 3. Expenses with incomplete metadata (missing vendor, category, or boolean taxDeductible)
442
+ if (expensesNeedingReview.length > 0) {
443
+ const reviewAmounts = expensesNeedingReview.map((e) => resolveDocumentAmount(e, defaultCurrency));
444
+ const reviewTotal = sumProvenancedAmounts(reviewAmounts, defaultCurrency);
445
+ priorities.push({
446
+ priority: "medium",
447
+ category: "expense_review",
448
+ title: `${expensesNeedingReview.length} gasto(s) con datos incompletos (proveedor, categoría o deducibilidad sin definir)`,
449
+ why: `Los gastos (${expensesNeedingReview.map((e) => `${String(e.id)} · ${String(e.description ?? "Sin descripción")}`).join(", ")}) carecen de proveedor, categoría o valor booleano en taxDeductible. (Nota: los gastos con taxDeductible=false y metadatos completos se contabilizan normalmente como no deducibles sin marcarse como error).`,
450
+ recordRefs: expensesNeedingReview.map((e) => ({
451
+ type: "expense",
452
+ id: String(e.id ?? ""),
453
+ label: `${String(e.description ?? e.id)} (${String(e.vendor ?? "sin proveedor")})`,
454
+ })),
455
+ amount: reviewTotal,
456
+ suggestedNextAction: {
457
+ description: "Comprobar cada gasto incompleto con get_expense y, si procede completar categoría, proveedor o taxDeductible, solicitar confirmación antes de llamar a update_expense.",
458
+ recommendedTool: "get_expense",
459
+ requiresConfirmation: true,
460
+ autoExecuted: false,
461
+ },
462
+ });
463
+ }
464
+ // 4. Client CRM follow-ups
465
+ for (const entry of activitiesByClient) {
466
+ const clientId = String(entry.client.id ?? "");
467
+ const clientName = String(entry.client.name ?? clientId);
468
+ if (entry.activities.length > 0) {
469
+ const latest = entry.activities[0];
470
+ priorities.push({
471
+ priority: "medium",
472
+ category: "client_followup",
473
+ title: `Seguimiento comercial registrado en ${clientName}`,
474
+ why: `Última actividad registrada (${String(latest.type ?? "actividad")}): "${String(latest.title ?? "")}" (${String(latest.id ?? "")}). Conviene verificar si requiere respuesta, presupuesto o nota de seguimiento.`,
475
+ recordRefs: [
476
+ { type: "client", id: clientId, label: clientName },
477
+ {
478
+ type: "client_activity",
479
+ id: String(latest.id ?? ""),
480
+ label: `${String(latest.type ?? "activity")}: ${String(latest.title ?? "")}`,
481
+ },
482
+ ],
483
+ suggestedNextAction: {
484
+ description: "Revisar fichas y notas del cliente con list_client_notes / list_client_contacts antes de registrar una nueva actividad confirmada.",
485
+ recommendedTool: "list_client_notes",
486
+ requiresConfirmation: false,
487
+ autoExecuted: false,
488
+ },
489
+ });
490
+ }
491
+ }
492
+ // 5. Plan quota alerts from get_business_context
493
+ const planObj = businessContext?.plan;
494
+ const usageObj = planObj?.usage;
495
+ if (usageObj) {
496
+ for (const [quotaKey, quotaVal] of Object.entries(usageObj)) {
497
+ const used = typeof quotaVal?.used === "number" ? quotaVal.used : 0;
498
+ const limit = typeof quotaVal?.limit === "number" ? quotaVal.limit : 0;
499
+ if (limit > 0 && used / limit >= 0.8) {
500
+ priorities.push({
501
+ priority: "low",
502
+ category: "plan_quota",
503
+ title: `Cuota mensual de ${quotaKey} al ${Math.round((used / limit) * 100)}% (${used}/${limit})`,
504
+ why: `El uso mensual reportado en get_business_context para '${quotaKey}' es ${used} de ${limit}.`,
505
+ recordRefs: [{ type: "plan_quota", id: quotaKey, label: `${quotaKey}: ${used}/${limit}` }],
506
+ suggestedNextAction: {
507
+ description: "Evitar creaciones de prueba innecesarias que consuman cuota mensual antes de cerrar el periodo.",
508
+ recommendedTool: "get_business_context",
509
+ requiresConfirmation: false,
510
+ autoExecuted: false,
511
+ },
512
+ });
513
+ }
514
+ }
515
+ }
516
+ // Known unknowns ("esto es lo que no puedo determinar")
517
+ const knownUnknowns = [
518
+ "Saldo bancario real y posición de tesorería: las herramientas de banca y conciliación bancaria no forman parte de esta consulta y la existencia de facturas no implica cobro efectivo en banco.",
519
+ "Saldos pendientes reales o cobros parciales: solo se observan los importes nominales íntegros y los estados de cada documento (p. ej. 'draft', 'sent', 'overdue', 'paid'), por lo que el importe de facturas no pagadas es un nominal bruto y no un saldo vivo conciliado.",
520
+ ];
521
+ if (isPartial) {
522
+ const partialLabels = Object.values(coverage.resources)
523
+ .filter((r) => !r.complete)
524
+ .map((r) => `${r.collection} (${r.inspected}/${r.total} vía ${r.sourceTool})`);
525
+ knownUnknowns.unshift(`Cobertura parcial del espacio de trabajo: solo se ha inspeccionado una página en ${partialLabels.join(", ")}. Los importes agregados reflejan únicamente los registros leídos en esta muestra y no el total histórico del negocio.`);
526
+ }
527
+ const draftInvoicesTotal = sumProvenancedAmounts(invoiceDrafts.map((d) => d.amount), defaultCurrency);
528
+ const draftQuotesTotal = sumProvenancedAmounts(quoteDrafts.map((d) => d.amount), defaultCurrency);
529
+ const issuedInvoicesTotal = sumProvenancedAmounts(issuedInvoices.map((d) => d.amount), defaultCurrency);
530
+ const paidInvoicesTotal = sumProvenancedAmounts(paidInvoices.map((d) => d.amount), defaultCurrency);
531
+ const outstandingInvoicesTotal = sumProvenancedAmounts(outstandingInvoices.map((d) => d.amount), defaultCurrency);
532
+ const overdueInvoicesTotal = sumProvenancedAmounts(overdueInvoices.map((d) => d.amount), defaultCurrency);
533
+ const onTimePendingTotal = sumProvenancedAmounts(onTimePendingInvoices.map((d) => d.amount), defaultCurrency);
534
+ const expensesTotal = sumProvenancedAmounts(expenseAmounts, defaultCurrency);
535
+ const deductibleTotal = sumProvenancedAmounts(deductibleExpenses.map((e) => resolveDocumentAmount(e, defaultCurrency)), defaultCurrency);
536
+ const nonDeductibleTotal = sumProvenancedAmounts(nonDeductibleExpenses.map((e) => resolveDocumentAmount(e, defaultCurrency)), defaultCurrency);
537
+ const issuedInvoicesInMonthTotal = sumProvenancedAmounts(issuedInvoicesInMonth.map((d) => d.amount), defaultCurrency);
538
+ const invoiceDraftsInMonthTotal = sumProvenancedAmounts(invoiceDraftsInMonth.map((d) => d.amount), defaultCurrency);
539
+ const expensesInMonthTotal = sumProvenancedAmounts(expensesInMonth.map((e) => resolveDocumentAmount(e, defaultCurrency)), defaultCurrency);
540
+ const markdownLines = [
541
+ `# Brief Operativo Diario (Corte operativo a ${asOfDate} · Muestra abierta/reciente sin filtro mensual)`,
542
+ "",
543
+ `## 1. Cobertura y Alcance Temporal (${isPartial ? "COBERTURA PARCIAL" : "COBERTURA COMPLETA"})`,
544
+ `- **Alcance temporal de la consulta**: muestra operativa a fecha \`${asOfDate}\` sobre los registros abiertos/recientes devueltos por \`list_invoices\`, \`list_quotes\` y \`list_expenses\` (incluye documentos de meses previos, como vencimientos pendientes, para no ocultar riesgos operativos). El subconjunto emitido/registrado en el mes \`${periodMonth}\` se desglosa aparte.`,
545
+ `- **Contexto del negocio**: \`get_business_context\``,
546
+ `- **Facturas** (\`list_invoices\`): ${invoicesCoverage.inspected}/${invoicesCoverage.total} (${invoicesCoverage.complete ? "completo" : "parcial"})`,
547
+ `- **Presupuestos** (\`list_quotes\`): ${quotesCoverage.inspected}/${quotesCoverage.total} (${quotesCoverage.complete ? "completo" : "parcial"})`,
548
+ `- **Gastos** (\`list_expenses\`): ${expensesCoverage.inspected}/${expensesCoverage.total} (${expensesCoverage.complete ? "completo" : "parcial"})`,
549
+ `- **Clientes** (\`list_clients\`): ${clientsCoverage.inspected}/${clientsCoverage.total} (${clientsCoverage.complete ? "completo" : "parcial"})`,
550
+ `- **Actividades CRM** (\`list_client_activities\`): ${clientActivitiesCoverage.inspected}/${clientActivitiesCoverage.total} clientes inspeccionados (${clientActivitiesCoverage.complete ? "completo" : "parcial"})`,
551
+ "",
552
+ "## 2. Separación entre Borrador, Facturación Emitida, Cobro Registrado e Importe Nominal No Pagado",
553
+ `- **Borradores de factura (NO emitidos / NO computan como ingreso ni saldo por cobrar)**: ${invoiceDrafts.length} documento(s) por **${formatProvenancedAmount(draftInvoicesTotal)}** (procedencia: \`${draftInvoicesTotal.provenance}\` — ${draftInvoicesTotal.fieldOrFormula}).`,
554
+ `- **Presupuestos en borrador/enviados**: ${quoteDrafts.length} documento(s) por **${formatProvenancedAmount(draftQuotesTotal)}** (procedencia: \`${draftQuotesTotal.provenance}\`).`,
555
+ `- **Facturación emitida en la muestra operativa (no borrador, incluye meses previos si siguen en la muestra)**: ${issuedInvoices.length} documento(s) por **${formatProvenancedAmount(issuedInvoicesTotal)}** (procedencia: \`${issuedInvoicesTotal.provenance}\` — ${issuedInvoicesTotal.fieldOrFormula}):`,
556
+ ` - **Cobro registrado en ERP (\`paid\`)**: ${paidInvoices.length} documento(s) por **${formatProvenancedAmount(paidInvoicesTotal)}**.`,
557
+ ` - **Importe nominal de facturas no marcadas como pagadas (saldo pendiente real no determinable sin cobros parciales)**: ${outstandingInvoices.length} documento(s) por **${formatProvenancedAmount(outstandingInvoicesTotal)}** (${overdueInvoices.length} vencida(s) por **${formatProvenancedAmount(overdueInvoicesTotal)}**; ${onTimePendingInvoices.length} en plazo por **${formatProvenancedAmount(onTimePendingTotal)}**).`,
558
+ `- **Gastos registrados en la muestra operativa**: ${expensesPage.data.length} registro(s) por **${formatProvenancedAmount(expensesTotal)}** (${deductibleExpenses.length} deducible(s) por ${formatProvenancedAmount(deductibleTotal)}; ${nonDeductibleExpenses.length} no deducible(s) por ${formatProvenancedAmount(nonDeductibleTotal)}; ${expensesNeedingReview.length} con datos incompletos pendientes de revisión).`,
559
+ `- **Subconjunto acotado al mes ${periodMonth} (filtrado por fecha del documento)**: ${issuedInvoicesInMonth.length} factura(s) emitida(s) por **${formatProvenancedAmount(issuedInvoicesInMonthTotal)}**, ${invoiceDraftsInMonth.length} borrador(es) de factura por **${formatProvenancedAmount(invoiceDraftsInMonthTotal)}** y ${expensesInMonth.length} gasto(s) por **${formatProvenancedAmount(expensesInMonthTotal)}**.`,
560
+ "",
561
+ "## 3. Qué Deberías Revisar Hoy y Por Qué (Priorizado)",
562
+ ...(priorities.length > 0
563
+ ? priorities.map((p, idx) => `${idx + 1}. **[${p.priority.toUpperCase()}] ${p.title}**\n` +
564
+ ` - **Por qué**: ${p.why}\n` +
565
+ ` - **Registros**: ${p.recordRefs.map((r) => `\`${r.id}\` (${r.label})`).join(", ")}\n` +
566
+ (p.amount
567
+ ? ` - **Importe y procedencia**: ${formatProvenancedAmount(p.amount)} (\`${p.amount.provenance}\`: ${p.amount.fieldOrFormula})\n`
568
+ : "") +
569
+ ` - **Siguiente acción sugerida (no ejecutada automáticamente)**: ${p.suggestedNextAction.description} (herramienta: \`${p.suggestedNextAction.recommendedTool}\`)`)
570
+ : ["- No se han detectado incidencias prioritarias en los registros inspeccionados."]),
571
+ "",
572
+ "## 4. Lo Que No Puedo Determinar con Estos Datos",
573
+ ...knownUnknowns.map((u) => `- ${u}`),
574
+ ];
575
+ return {
576
+ question: "¿Qué debería revisar hoy en mi negocio y por qué?",
577
+ coverage,
578
+ activitySeparation: {
579
+ draftActivity: {
580
+ note: "Los borradores reservan numeración pero no han sido emitidos ni enviados al cliente; no representan ingresos devengados ni cobrados.",
581
+ invoiceDraftsCount: invoiceDrafts.length,
582
+ invoiceDraftsTotal: draftInvoicesTotal,
583
+ invoiceDrafts,
584
+ quoteDraftsCount: quoteDrafts.length,
585
+ quoteDraftsTotal: draftQuotesTotal,
586
+ quoteDrafts,
587
+ },
588
+ issuedActivity: {
589
+ note: "Incluye únicamente facturas emitidas (estado distinto de 'draft' y 'cancelled'), separando cobro registrado ('paid') del importe nominal de facturas no marcadas como pagadas ('overdue' vs en plazo). El saldo pendiente real no es determinable sin desglose de cobros parciales ni conciliación bancaria.",
590
+ realOutstandingBalanceDeterminable: false,
591
+ unpaidNominalSemantics: "nominal_unpaid_invoice_totals_without_partial_payments",
592
+ issuedInvoicesCount: issuedInvoices.length,
593
+ issuedInvoicesByStatus,
594
+ issuedInvoicesTotal,
595
+ issuedInvoices,
596
+ paidInvoicesCount: paidInvoices.length,
597
+ paidInvoicesTotal,
598
+ paidInvoices,
599
+ unpaidNominalInvoicesCount: outstandingInvoices.length,
600
+ unpaidNominalTotal: outstandingInvoicesTotal,
601
+ unpaidNominalTotalsByCurrency: outstandingInvoicesTotal.totalsByCurrency ?? {},
602
+ overdueNominalInvoicesCount: overdueInvoices.length,
603
+ overdueNominalTotal: overdueInvoicesTotal,
604
+ onTimeNominalPendingCount: onTimePendingInvoices.length,
605
+ onTimeNominalPendingTotal: onTimePendingTotal,
606
+ outstandingInvoicesCount: outstandingInvoices.length,
607
+ outstandingInvoicesTotal,
608
+ overdueInvoicesCount: overdueInvoices.length,
609
+ overdueInvoicesTotal,
610
+ onTimePendingCount: onTimePendingInvoices.length,
611
+ onTimePendingTotal,
612
+ outstandingInvoices,
613
+ },
614
+ recordedExpenses: {
615
+ note: "Los gastos con taxDeductible=false y proveedor/categoría completos se desglosan como no deducibles sin tratarse como error; solo se marcan para revisión los registros con proveedor, categoría o deducibilidad sin especificar.",
616
+ expensesCount: expensesPage.data.length,
617
+ expensesTotal,
618
+ deductibleCount: deductibleExpenses.length,
619
+ deductibleTotal,
620
+ nonDeductibleCount: nonDeductibleExpenses.length,
621
+ nonDeductibleTotal,
622
+ pendingReviewCount: expensesNeedingReview.length,
623
+ },
624
+ currentMonthActivity: {
625
+ referenceMonth: periodMonth,
626
+ note: `Subconjunto de documentos cuya fecha de emisión (issueDate) o fecha de gasto (date) pertenece explícitamente al mes ${periodMonth}, separado de los registros de meses previos presentes en la muestra operativa.`,
627
+ issuedInvoicesInMonthCount: issuedInvoicesInMonth.length,
628
+ issuedInvoicesInMonthTotal,
629
+ invoiceDraftsInMonthCount: invoiceDraftsInMonth.length,
630
+ invoiceDraftsInMonthTotal,
631
+ expensesInMonthCount: expensesInMonth.length,
632
+ expensesInMonthTotal,
633
+ },
634
+ },
635
+ priorities,
636
+ knownUnknowns,
637
+ mutationsExecuted: 0,
638
+ markdown: markdownLines.join("\n"),
639
+ };
640
+ }
641
+ export function buildScenario1CompleteWorkspaceClient(callLog) {
642
+ return {
643
+ getBusinessContext: async () => {
644
+ callLog.push("get_business_context");
645
+ return {
646
+ company: { name: "Estudio Atlántico SL", currency: "EUR", taxRegime: "iva" },
647
+ monthlySummary: { invoiced: 1347, expenses: 153.5 },
648
+ plan: {
649
+ tier: "free",
650
+ usage: {
651
+ invoices: { used: 3, limit: 10 },
652
+ expenses: { used: 4, limit: 5 },
653
+ },
654
+ },
655
+ };
656
+ },
657
+ listInvoices: async () => {
658
+ callLog.push("list_invoices");
659
+ return {
660
+ data: [
661
+ {
662
+ id: "inv_draft_1",
663
+ documentNumber: "F20260001",
664
+ status: "draft",
665
+ clientName: "Acme SL",
666
+ issueDate: "2026-10-08",
667
+ dueDate: "2026-11-08",
668
+ total: 968,
669
+ currency: "EUR",
670
+ },
671
+ {
672
+ id: "inv_draft_4",
673
+ documentNumber: "F20260004",
674
+ status: "draft",
675
+ clientName: "Acme SL",
676
+ issueDate: "2026-10-27",
677
+ dueDate: "2026-11-27",
678
+ total: 193.6,
679
+ currency: "EUR",
680
+ },
681
+ {
682
+ id: "inv_overdue_0",
683
+ documentNumber: "F20260000",
684
+ status: "overdue",
685
+ clientName: "OpenAI Demo Sample Client",
686
+ issueDate: "2026-09-10",
687
+ dueDate: "2026-10-10",
688
+ total: 605,
689
+ currency: "EUR",
690
+ },
691
+ {
692
+ id: "inv_sent_2",
693
+ documentNumber: "F20260002",
694
+ status: "sent",
695
+ clientName: "Acme SL",
696
+ issueDate: "2026-10-20",
697
+ dueDate: "2026-11-20",
698
+ total: 847,
699
+ currency: "EUR",
700
+ },
701
+ {
702
+ id: "inv_paid_3",
703
+ documentNumber: "F20260003",
704
+ status: "paid",
705
+ clientName: "Acme SL",
706
+ issueDate: "2026-10-05",
707
+ dueDate: "2026-10-20",
708
+ total: 500,
709
+ currency: "EUR",
710
+ },
711
+ ],
712
+ total: 5,
713
+ limit: 20,
714
+ offset: 0,
715
+ };
716
+ },
717
+ listQuotes: async () => {
718
+ callLog.push("list_quotes");
719
+ return {
720
+ data: [
721
+ {
722
+ id: "quo_draft_1",
723
+ documentNumber: "P20260001",
724
+ status: "draft",
725
+ clientName: "Acme SL",
726
+ issueDate: "2026-10-25",
727
+ validUntil: "2026-11-10",
728
+ total: 1210,
729
+ currency: "EUR",
730
+ },
731
+ ],
732
+ total: 1,
733
+ limit: 20,
734
+ offset: 0,
735
+ };
736
+ },
737
+ listExpenses: async () => {
738
+ callLog.push("list_expenses");
739
+ return {
740
+ data: [
741
+ {
742
+ id: "jyaSd4KNgig9B5pGcsWP",
743
+ description: "Office supplies",
744
+ amount: 45.99,
745
+ category: "office",
746
+ vendor: "Office Depot Canarias",
747
+ taxDeductible: false,
748
+ date: "2026-08-28",
749
+ },
750
+ {
751
+ id: "J5ICnB9lZMokSapUPByP",
752
+ description: "Servidores QA octubre",
753
+ amount: 121.0,
754
+ category: "software",
755
+ vendor: "Cloud Hosting QA",
756
+ taxDeductible: true,
757
+ date: "2026-10-27",
758
+ },
759
+ {
760
+ id: "exp_incomplete_1",
761
+ description: "Recibo taxi sin categorizar",
762
+ amount: 32.5,
763
+ category: "",
764
+ vendor: "",
765
+ date: "2026-10-27",
766
+ },
767
+ ],
768
+ total: 3,
769
+ limit: 20,
770
+ offset: 0,
771
+ };
772
+ },
773
+ listClients: async () => {
774
+ callLog.push("list_clients");
775
+ return {
776
+ data: [
777
+ { id: "444IZvNpVrtJhj5nRySW", name: "Acme SL", email: "contact@acme.example" },
778
+ { id: "RCjuHqsCchF5ZCB7gU0s", name: "OpenAI Demo Sample Client", email: "demo@example.com" },
779
+ ],
780
+ total: 2,
781
+ limit: 20,
782
+ offset: 0,
783
+ };
784
+ },
785
+ listClientActivities: async (clientId) => {
786
+ callLog.push(`list_client_activities:${clientId}`);
787
+ if (clientId === "444IZvNpVrtJhj5nRySW") {
788
+ return {
789
+ data: [
790
+ {
791
+ id: "act_call_1",
792
+ type: "call",
793
+ title: "Llamada de seguimiento sobre propuesta de octubre",
794
+ createdAt: "2026-10-28T01:18:39.677Z",
795
+ },
796
+ ],
797
+ total: 1,
798
+ limit: 10,
799
+ offset: 0,
800
+ };
801
+ }
802
+ return { data: [], total: 0, limit: 10, offset: 0 };
803
+ },
804
+ };
805
+ }
806
+ export function buildScenario2PartialCoverageClient(callLog) {
807
+ return {
808
+ getBusinessContext: async () => {
809
+ callLog.push("get_business_context");
810
+ return {
811
+ company: { name: "Distribuciones Insulares SL", currency: "EUR" },
812
+ plan: { tier: "pro", usage: { invoices: { used: 47, limit: 500 } } },
813
+ };
814
+ },
815
+ listInvoices: async () => {
816
+ callLog.push("list_invoices");
817
+ return {
818
+ data: [
819
+ {
820
+ id: "inv_legacy_draft",
821
+ documentNumber: "F20260047",
822
+ status: "draft",
823
+ clientName: "Logística Norte SL",
824
+ issueDate: "2026-10-27",
825
+ items: [{ description: "Consultoría logística", quantity: 3, unitPrice: 100 }],
826
+ taxRate: 21,
827
+ },
828
+ {
829
+ id: "inv_sent_46",
830
+ documentNumber: "F20260046",
831
+ status: "sent",
832
+ clientName: "Marítima Sur SL",
833
+ issueDate: "2026-09-15",
834
+ dueDate: "2026-10-15",
835
+ total: 2420,
836
+ },
837
+ ],
838
+ total: 47,
839
+ limit: 2,
840
+ offset: 0,
841
+ };
842
+ },
843
+ listQuotes: async () => {
844
+ callLog.push("list_quotes");
845
+ return { data: [], total: 0, limit: 2, offset: 0 };
846
+ },
847
+ listExpenses: async () => {
848
+ callLog.push("list_expenses");
849
+ return {
850
+ data: [
851
+ {
852
+ id: "exp_missing_vendor",
853
+ description: "Combustible flota",
854
+ amount: 88.5,
855
+ category: "",
856
+ vendor: "",
857
+ taxDeductible: true,
858
+ date: "2026-10-26",
859
+ },
860
+ ],
861
+ total: 35,
862
+ limit: 1,
863
+ offset: 0,
864
+ };
865
+ },
866
+ listClients: async () => {
867
+ callLog.push("list_clients");
868
+ return {
869
+ data: [{ id: "cli_1", name: "Logística Norte SL" }],
870
+ total: 12,
871
+ limit: 1,
872
+ offset: 0,
873
+ };
874
+ },
875
+ listClientActivities: async (clientId) => {
876
+ callLog.push(`list_client_activities:${clientId}`);
877
+ return { data: [], total: 0, limit: 10, offset: 0 };
878
+ },
879
+ };
880
+ }
881
+ //# sourceMappingURL=daily-operational-brief.js.map