@yoryoboy/bi-mcp 1.19.0 → 1.20.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 (53) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/mcp-use.json +6 -3
  3. package/dist/scripts/profile-mercadolibre-billing.js +634 -0
  4. package/dist/scripts/profile-mercadolibre-billing.js.map +7 -0
  5. package/dist/src/config/__tests__/tiendanube-profile-store.test.js +50 -0
  6. package/dist/src/config/__tests__/tiendanube-profile-store.test.js.map +7 -0
  7. package/dist/src/config/tiendanube-profile-store.js +31 -0
  8. package/dist/src/config/tiendanube-profile-store.js.map +7 -0
  9. package/dist/src/google-ads/report-utils.js +78 -0
  10. package/dist/src/google-ads/report-utils.js.map +7 -0
  11. package/dist/src/search-console/search-console-utils.js +275 -0
  12. package/dist/src/search-console/search-console-utils.js.map +7 -0
  13. package/dist/src/services/mercadolibre/__tests__/mercadolibre-sales-report-ventas.test.js +358 -0
  14. package/dist/src/services/mercadolibre/__tests__/mercadolibre-sales-report-ventas.test.js.map +7 -0
  15. package/dist/src/services/mercadolibre/__tests__/mercadolibre-sales-report.test.js +96 -0
  16. package/dist/src/services/mercadolibre/__tests__/mercadolibre-sales-report.test.js.map +7 -0
  17. package/dist/src/services/mercadolibre/mercadolibre-claim-return-lookup.js +15 -0
  18. package/dist/src/services/mercadolibre/mercadolibre-claim-return-lookup.js.map +7 -0
  19. package/dist/src/services/mercadolibre/mercadolibre-observability.js +12 -0
  20. package/dist/src/services/mercadolibre/mercadolibre-observability.js.map +7 -0
  21. package/dist/src/services/mercadolibre/mercadolibre-sale-reference.js +7 -0
  22. package/dist/src/services/mercadolibre/mercadolibre-sale-reference.js.map +7 -0
  23. package/dist/src/services/mercadolibre/mercadolibre-sales-report.js +553 -0
  24. package/dist/src/services/mercadolibre/mercadolibre-sales-report.js.map +7 -0
  25. package/dist/src/services/vtex/__tests__/vtex-catalog-write-batch.test.js +165 -0
  26. package/dist/src/services/vtex/__tests__/vtex-catalog-write-batch.test.js.map +7 -0
  27. package/dist/src/tools/config/__tests__/list-profiles.test.js +144 -1
  28. package/dist/src/tools/config/__tests__/list-profiles.test.js.map +2 -2
  29. package/dist/src/tools/config/list-profiles.js +50 -6
  30. package/dist/src/tools/config/list-profiles.js.map +2 -2
  31. package/dist/src/tools/mercadolibre/__tests__/get-sales-report-foundation.test.js +417 -0
  32. package/dist/src/tools/mercadolibre/__tests__/get-sales-report-foundation.test.js.map +7 -0
  33. package/dist/src/tools/mercadolibre/get-sales-report.js +474 -0
  34. package/dist/src/tools/mercadolibre/get-sales-report.js.map +7 -0
  35. package/dist/src/tools/vtex/__tests__/catalog-admin-batch.test.js +233 -0
  36. package/dist/src/tools/vtex/__tests__/catalog-admin-batch.test.js.map +7 -0
  37. package/dist/src/tools/vtex/catalog-admin-batch.js +298 -0
  38. package/dist/src/tools/vtex/catalog-admin-batch.js.map +7 -0
  39. package/dist/tests/meli/meli-sales-report-buyers-mcp-e2e.test.js +137 -0
  40. package/dist/tests/meli/meli-sales-report-buyers-mcp-e2e.test.js.map +7 -0
  41. package/dist/tests/meli/meli-sales-report-mcp-e2e-server.js +24 -0
  42. package/dist/tests/meli/meli-sales-report-mcp-e2e-server.js.map +7 -0
  43. package/dist/tests/meli/meli-sales-report-mcp-e2e.test.js +207 -0
  44. package/dist/tests/meli/meli-sales-report-mcp-e2e.test.js.map +7 -0
  45. package/dist/tests/meli/meli-sales-report-public-mcp-e2e-server.js +10 -0
  46. package/dist/tests/meli/meli-sales-report-public-mcp-e2e-server.js.map +7 -0
  47. package/dist/tests/meli/meli-sales-report-public-registration.test.js +98 -0
  48. package/dist/tests/meli/meli-sales-report-public-registration.test.js.map +7 -0
  49. package/dist/tests/smoke/process-harness.js +89 -0
  50. package/dist/tests/smoke/process-harness.js.map +7 -0
  51. package/dist/tests/smoke/process-harness.test.js +39 -0
  52. package/dist/tests/smoke/process-harness.test.js.map +7 -0
  53. package/package.json +1 -1
package/dist/mcp-use.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "includeInspector": false,
3
- "buildTime": "2026-07-31T15:02:12.296Z",
4
- "buildId": "8b32be6eea3e9dc2",
3
+ "buildTime": "2026-08-03T01:50:34.212Z",
4
+ "buildId": "01bbc68597ae128e",
5
5
  "entryPoint": "dist/index.js",
6
- "widgets": {}
6
+ "widgets": {},
7
+ "tunnel": {
8
+ "subdomain": "historic-amethyst"
9
+ }
7
10
  }
@@ -0,0 +1,634 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import process from "node:process";
3
+ import {
4
+ formatMercadoLibreError,
5
+ getMercadoLibreApiContext,
6
+ isRetryableMercadoLibreError,
7
+ MercadoLibreApiError
8
+ } from "../src/services/mercadolibre/mercadolibre-api.js";
9
+ import {
10
+ buildMercadoLibreBillingPeriodDetailsPath,
11
+ buildMercadoLibreBillingOrderDetailsPath,
12
+ clampMercadoLibreBillingOrderIdsBatchSize,
13
+ MERCADO_LIBRE_BILLING_ORDER_IDS_MAX_PER_REQUEST,
14
+ scopeToGroupPath
15
+ } from "../src/services/mercadolibre/mercadolibre-billing.js";
16
+ import { loadLocalEnv, parseCliArgs } from "./_helpers.js";
17
+ const HELP = `Mercado Libre billing profiler
18
+
19
+ Usage:
20
+ pnpm exec tsx scripts/profile-mercadolibre-billing.ts --profile-id <id> --period-key <key> [options]
21
+
22
+ Required:
23
+ --profile-id, MELI_PROFILE_ID, MERCADOLIBRE_PROFILE_ID
24
+ --period-key, MELI_PERIOD_KEY
25
+
26
+ Optional:
27
+ --document-type BILL|CREDIT_NOTE Default: BILL
28
+ --mode period|order-ids|both Default: both
29
+ --scope ml|mp|flex|full Default: full
30
+ --limit <n> Default: 1000
31
+ --max-pages <n> Default: 10
32
+ --batch-size <n> Default: 60 (capped at 60 per request)
33
+ --delay-ms <n> Default: 150
34
+ --max-retries <n> Default: 0
35
+ --order-ids <id,id,...> Required when mode includes order-ids
36
+ --output <path> Default: /tmp/opencode/mercadolibre-billing-profile-<timestamp>.json
37
+ --help
38
+
39
+ Examples:
40
+ pnpm exec tsx scripts/profile-mercadolibre-billing.ts --profile-id profile_123 --period-key 2025-01 --mode both --order-ids 123,456,789
41
+ `;
42
+ function printHelp() {
43
+ console.log(HELP);
44
+ }
45
+ function sleep(ms) {
46
+ return new Promise((resolve) => setTimeout(resolve, ms));
47
+ }
48
+ function nowIso() {
49
+ return (/* @__PURE__ */ new Date()).toISOString();
50
+ }
51
+ function toPositiveInteger(value, fallback, name) {
52
+ if (!value || !value.trim()) {
53
+ return fallback;
54
+ }
55
+ const parsed = Number(value);
56
+ if (!Number.isInteger(parsed) || parsed < 0) {
57
+ throw new Error(`Invalid ${name}: expected a non-negative integer, got ${value}`);
58
+ }
59
+ return parsed;
60
+ }
61
+ function normalizeMode(value) {
62
+ const candidate = value?.trim().toLowerCase();
63
+ if (candidate === "period" || candidate === "order-ids" || candidate === "both") {
64
+ return candidate;
65
+ }
66
+ if (!candidate) {
67
+ return "both";
68
+ }
69
+ throw new Error(`Invalid mode: ${value}`);
70
+ }
71
+ function normalizeScope(value) {
72
+ const candidate = value?.trim().toLowerCase();
73
+ if (candidate === "ml" || candidate === "mp" || candidate === "flex" || candidate === "full") {
74
+ return candidate;
75
+ }
76
+ if (!candidate) {
77
+ return "full";
78
+ }
79
+ throw new Error(`Invalid scope: ${value}`);
80
+ }
81
+ function normalizeDocumentType(value) {
82
+ const candidate = value?.trim().toUpperCase();
83
+ if (candidate === "BILL" || candidate === "CREDIT_NOTE") {
84
+ return candidate;
85
+ }
86
+ if (!candidate) {
87
+ return "BILL";
88
+ }
89
+ throw new Error(`Invalid document type: ${value}`);
90
+ }
91
+ function parseOrderIds(raw) {
92
+ if (!raw) {
93
+ return [];
94
+ }
95
+ return raw.split(/[\s,]+/g).map((value) => value.trim()).filter(Boolean);
96
+ }
97
+ function readConfigValue(args, name, ...envNames) {
98
+ const cliValue = args.values[name]?.trim();
99
+ if (cliValue) {
100
+ return cliValue;
101
+ }
102
+ for (const envName of envNames) {
103
+ const envValue = process.env[envName]?.trim();
104
+ if (envValue) {
105
+ return envValue;
106
+ }
107
+ }
108
+ return void 0;
109
+ }
110
+ function parseConfig() {
111
+ const args = parseCliArgs(process.argv.slice(2));
112
+ if (args.flags.has("help") || args.flags.has("h")) {
113
+ printHelp();
114
+ process.exit(0);
115
+ }
116
+ const profileId = readConfigValue(args, "profile-id", "MELI_PROFILE_ID", "MERCADOLIBRE_PROFILE_ID");
117
+ const periodKey = readConfigValue(args, "period-key", "MELI_PERIOD_KEY");
118
+ const documentType = normalizeDocumentType(readConfigValue(args, "document-type", "MELI_DOCUMENT_TYPE"));
119
+ const mode = normalizeMode(readConfigValue(args, "mode", "MELI_MODE"));
120
+ const scope = normalizeScope(readConfigValue(args, "scope", "MELI_SCOPE"));
121
+ const pageLimit = toPositiveInteger(readConfigValue(args, "limit", "MELI_LIMIT"), 1e3, "limit");
122
+ const maxPages = toPositiveInteger(readConfigValue(args, "max-pages", "MELI_MAX_PAGES"), 10, "max-pages");
123
+ const batchSize = toPositiveInteger(readConfigValue(args, "batch-size", "MELI_BATCH_SIZE"), 60, "batch-size");
124
+ const delayMs = toPositiveInteger(readConfigValue(args, "delay-ms", "MELI_DELAY_MS"), 150, "delay-ms");
125
+ const maxRetries = toPositiveInteger(readConfigValue(args, "max-retries", "MELI_MAX_RETRIES"), 0, "max-retries");
126
+ const orderIds = parseOrderIds(readConfigValue(args, "order-ids", "MELI_ORDER_IDS"));
127
+ const outputPath = readConfigValue(args, "output", "MELI_OUTPUT_PATH") ?? `/tmp/opencode/mercadolibre-billing-profile-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.json`;
128
+ if (!profileId) {
129
+ throw new Error("Missing required profile id. Pass --profile-id or set MELI_PROFILE_ID.");
130
+ }
131
+ if (!periodKey) {
132
+ throw new Error("Missing required period key. Pass --period-key or set MELI_PERIOD_KEY.");
133
+ }
134
+ if ((mode === "order-ids" || mode === "both") && orderIds.length === 0) {
135
+ throw new Error("Order IDs are required when mode includes order-ids. Pass --order-ids or MELI_ORDER_IDS.");
136
+ }
137
+ return {
138
+ profileId,
139
+ periodKey,
140
+ documentType,
141
+ mode,
142
+ scope,
143
+ pageLimit,
144
+ maxPages,
145
+ batchSize,
146
+ delayMs,
147
+ maxRetries,
148
+ orderIds,
149
+ outputPath
150
+ };
151
+ }
152
+ function createStrategyStats() {
153
+ return {
154
+ requestsAttempted: 0,
155
+ requestsSucceeded: 0,
156
+ requestsFailed: 0,
157
+ transportAttempts: 0,
158
+ rateLimit429Count: 0,
159
+ rowsRecovered: 0,
160
+ rawRowsRecovered: 0,
161
+ elapsedMs: 0,
162
+ totalLatencyMs: 0,
163
+ rowKeys: /* @__PURE__ */ new Set(),
164
+ lastIdsSeen: [],
165
+ requestLogs: [],
166
+ coverageComplete: null,
167
+ stopReason: null
168
+ };
169
+ }
170
+ function getHeaderValue(headers, name) {
171
+ if (!headers) {
172
+ return null;
173
+ }
174
+ const raw = headers[name.toLowerCase()] ?? headers[name] ?? headers[name.toUpperCase()];
175
+ if (Array.isArray(raw)) {
176
+ return raw.length > 0 ? String(raw[0]) : null;
177
+ }
178
+ return raw == null ? null : String(raw);
179
+ }
180
+ function parseRetryAfterHeaderMs(value) {
181
+ if (!value) {
182
+ return null;
183
+ }
184
+ const seconds = Number(value);
185
+ if (Number.isFinite(seconds) && seconds > 0) {
186
+ return seconds * 1e3;
187
+ }
188
+ const dateMs = new Date(value).getTime();
189
+ if (Number.isFinite(dateMs)) {
190
+ return Math.max(0, dateMs - Date.now());
191
+ }
192
+ return null;
193
+ }
194
+ function asRecord(value) {
195
+ return value != null && typeof value === "object" && !Array.isArray(value) ? value : {};
196
+ }
197
+ function normalizeCursor(value) {
198
+ if (value == null || value === 0 || value === "0") {
199
+ return null;
200
+ }
201
+ const normalized = String(value).trim();
202
+ return normalized ? normalized : null;
203
+ }
204
+ function getRowKey(strategy, row) {
205
+ if (strategy === "period") {
206
+ const chargeInfo = asRecord(row.charge_info);
207
+ const detailId2 = String(chargeInfo.detail_id ?? row.detail_id ?? "").trim();
208
+ if (detailId2) {
209
+ return `detail:${detailId2}`;
210
+ }
211
+ }
212
+ const orderId = String(row.order_id ?? "").trim();
213
+ if (orderId) {
214
+ return `order:${orderId}`;
215
+ }
216
+ const detailId = String(row.detail_id ?? "").trim();
217
+ if (detailId) {
218
+ return `detail:${detailId}`;
219
+ }
220
+ return null;
221
+ }
222
+ function summarizeError(error) {
223
+ if (error instanceof MercadoLibreApiError) {
224
+ return {
225
+ name: error.name,
226
+ message: error.message,
227
+ status: error.status ?? null,
228
+ code: error.code ?? null,
229
+ retryAfterMs: error.retryAfterMs ?? null,
230
+ retryable: isRetryableMercadoLibreError(error)
231
+ };
232
+ }
233
+ if (error instanceof Error) {
234
+ return {
235
+ name: error.name,
236
+ message: formatMercadoLibreError(error),
237
+ status: null,
238
+ code: null,
239
+ retryAfterMs: null,
240
+ retryable: null
241
+ };
242
+ }
243
+ return {
244
+ name: "UnknownError",
245
+ message: formatMercadoLibreError(error),
246
+ status: null,
247
+ code: null,
248
+ retryAfterMs: null,
249
+ retryable: null
250
+ };
251
+ }
252
+ function logRequest(summary) {
253
+ console.log(JSON.stringify(summary));
254
+ }
255
+ async function profiledGet(api, config) {
256
+ const startedAt = nowIso();
257
+ const startedMs = Date.now();
258
+ try {
259
+ const response = await api.get(config.endpoint, { params: config.params });
260
+ const endedAt = nowIso();
261
+ const durationMs = Date.now() - startedMs;
262
+ const data = response.data;
263
+ const responseRecord = asRecord(data);
264
+ const results = Array.isArray(responseRecord.results) ? responseRecord.results : [];
265
+ const lastId = normalizeCursor(responseRecord.last_id);
266
+ const total = Number.isFinite(Number(responseRecord.total)) ? Number(responseRecord.total) : null;
267
+ const retryAfterHeader = getHeaderValue(response.headers, "retry-after");
268
+ const log = {
269
+ startedAt,
270
+ endedAt,
271
+ durationMs,
272
+ strategy: config.strategy,
273
+ step: config.step,
274
+ attempt: config.attempt,
275
+ endpoint: config.endpoint,
276
+ scope: config.scope,
277
+ groupPath: config.groupPath,
278
+ params: config.params,
279
+ status: response.status,
280
+ resultCount: results.length,
281
+ total,
282
+ lastId,
283
+ responseShape: Object.keys(responseRecord).sort(),
284
+ retryAfterMs: parseRetryAfterHeaderMs(retryAfterHeader),
285
+ retryAfterHeader
286
+ };
287
+ logRequest(log);
288
+ return { ok: true, data, log };
289
+ } catch (error) {
290
+ const endedAt = nowIso();
291
+ const durationMs = Date.now() - startedMs;
292
+ const summary = summarizeError(error);
293
+ const log = {
294
+ startedAt,
295
+ endedAt,
296
+ durationMs,
297
+ strategy: config.strategy,
298
+ step: config.step,
299
+ attempt: config.attempt,
300
+ endpoint: config.endpoint,
301
+ scope: config.scope,
302
+ groupPath: config.groupPath,
303
+ params: config.params,
304
+ status: summary.status,
305
+ resultCount: null,
306
+ total: null,
307
+ lastId: null,
308
+ responseShape: [],
309
+ retryAfterMs: summary.retryAfterMs,
310
+ retryAfterHeader: null,
311
+ error: summary
312
+ };
313
+ logRequest(log);
314
+ return { ok: false, log };
315
+ }
316
+ }
317
+ async function waitBetweenRequests(delayMs) {
318
+ if (delayMs > 0) {
319
+ await sleep(delayMs);
320
+ }
321
+ }
322
+ async function requestWithRetries(api, config, maxRetries, delayMs) {
323
+ const logs = [];
324
+ for (let attempt = 1; attempt <= maxRetries + 1; attempt += 1) {
325
+ const result = await profiledGet(api, { ...config, attempt });
326
+ logs.push(result.log);
327
+ if (result.ok) {
328
+ return { ok: true, data: result.data, logs };
329
+ }
330
+ const error = result.log.error;
331
+ const retryable = error?.retryable ?? false;
332
+ const isLastAttempt = attempt >= maxRetries + 1;
333
+ if (isLastAttempt || !retryable) {
334
+ return { ok: false, logs };
335
+ }
336
+ const backoffMs = Math.max(delayMs, error?.retryAfterMs ?? 0, 200 * attempt);
337
+ await sleep(backoffMs);
338
+ }
339
+ return { ok: false, logs };
340
+ }
341
+ function summarizeResponseRows(strategy, rows, stats) {
342
+ stats.rawRowsRecovered += rows.length;
343
+ for (const row of rows) {
344
+ const key = getRowKey(strategy, row);
345
+ if (!key) {
346
+ continue;
347
+ }
348
+ stats.rowKeys.add(key);
349
+ }
350
+ stats.rowsRecovered = stats.rowKeys.size;
351
+ }
352
+ function buildStrategyReport(stats) {
353
+ return {
354
+ requestsAttempted: stats.requestsAttempted,
355
+ requestsSucceeded: stats.requestsSucceeded,
356
+ requestsFailed: stats.requestsFailed,
357
+ transportAttempts: stats.transportAttempts,
358
+ rateLimit429Count: stats.rateLimit429Count,
359
+ rowsRecovered: stats.rowsRecovered,
360
+ rawRowsRecovered: stats.rawRowsRecovered,
361
+ elapsedMs: stats.elapsedMs,
362
+ totalLatencyMs: stats.totalLatencyMs,
363
+ averageLatencyMs: stats.requestsSucceeded > 0 ? Math.round(stats.totalLatencyMs / stats.requestsSucceeded) : 0,
364
+ coverageComplete: stats.coverageComplete,
365
+ stopReason: stats.stopReason,
366
+ lastIdsSeen: stats.lastIdsSeen,
367
+ requestLogs: stats.requestLogs
368
+ };
369
+ }
370
+ function recommendStrategy(period, orderIds) {
371
+ if (!period && !orderIds) {
372
+ return "No strategy executed.";
373
+ }
374
+ if (period && !orderIds) {
375
+ return period.rateLimit429Count > 0 ? "Period cursor worked, but 429s appeared. Reduce page size or add delay before scaling up." : "Period cursor is the only observed strategy. Use it as the baseline and keep batch size conservative.";
376
+ }
377
+ if (!period && orderIds) {
378
+ return orderIds.rateLimit429Count > 0 ? "Order-ID batches worked, but 429s appeared. Lower the batch size and add more delay." : "Order-ID batches are the only observed strategy. Use them when the report generator already has the target order set.";
379
+ }
380
+ if (!period || !orderIds) {
381
+ return "No recommendation available.";
382
+ }
383
+ if (period.rateLimit429Count !== orderIds.rateLimit429Count) {
384
+ return period.rateLimit429Count < orderIds.rateLimit429Count ? "Prefer period pagination: it produced fewer 429s under the current pacing." : "Prefer order-id batching: it produced fewer 429s under the current pacing.";
385
+ }
386
+ const periodThroughput = period.elapsedMs > 0 ? period.rowsRecovered / (period.elapsedMs / 1e3) : 0;
387
+ const orderThroughput = orderIds.elapsedMs > 0 ? orderIds.rowsRecovered / (orderIds.elapsedMs / 1e3) : 0;
388
+ if (Math.abs(periodThroughput - orderThroughput) > 0.01) {
389
+ return periodThroughput > orderThroughput ? "Prefer period pagination: it recovered rows faster with the current pacing." : "Prefer order-id batching: it recovered rows faster with the current pacing.";
390
+ }
391
+ if (Boolean(period.coverageComplete) !== Boolean(orderIds.coverageComplete)) {
392
+ return period.coverageComplete ? "Prefer period pagination: it completed coverage while order-id batching did not." : "Prefer order-id batching: it completed coverage while period pagination did not.";
393
+ }
394
+ return "Both strategies behaved similarly. Use period pagination for bulk coverage and order-id batching for targeted subsets.";
395
+ }
396
+ async function profilePeriodStrategy(api, config, startedAtMs) {
397
+ const stats = createStrategyStats();
398
+ let fromId = null;
399
+ let page = 0;
400
+ let coverageComplete = true;
401
+ while (page < config.maxPages) {
402
+ page += 1;
403
+ stats.requestsAttempted += 1;
404
+ const groupPath = scopeToGroupPath(config.scope);
405
+ const endpoint = buildMercadoLibreBillingPeriodDetailsPath(config.periodKey, config.scope);
406
+ const params = {
407
+ document_type: config.documentType,
408
+ limit: config.pageLimit
409
+ };
410
+ if (fromId) {
411
+ params.from_id = fromId;
412
+ }
413
+ const result = await requestWithRetries(
414
+ api,
415
+ {
416
+ strategy: "period",
417
+ step: `page-${page}`,
418
+ endpoint,
419
+ scope: config.scope,
420
+ groupPath,
421
+ params
422
+ },
423
+ config.maxRetries,
424
+ config.delayMs
425
+ );
426
+ stats.requestLogs.push(...result.logs);
427
+ stats.transportAttempts += result.logs.length;
428
+ stats.rateLimit429Count += result.logs.filter((log) => log.status === 429).length;
429
+ if (!result.ok) {
430
+ stats.requestsFailed += 1;
431
+ stats.stopReason = `Stopped on page ${page} after a failed request`;
432
+ coverageComplete = false;
433
+ break;
434
+ }
435
+ stats.requestsSucceeded += 1;
436
+ const lastLog = result.logs[result.logs.length - 1];
437
+ stats.totalLatencyMs += lastLog?.durationMs ?? 0;
438
+ const responseRecord = asRecord(result.data);
439
+ const rows = Array.isArray(responseRecord.results) ? responseRecord.results.filter((entry) => entry != null && typeof entry === "object" && !Array.isArray(entry)) : [];
440
+ summarizeResponseRows("period", rows, stats);
441
+ const nextLastId = normalizeCursor(responseRecord.last_id);
442
+ if (nextLastId) {
443
+ stats.lastIdsSeen.push(nextLastId);
444
+ }
445
+ if (!nextLastId) {
446
+ stats.stopReason = "Reached terminal last_id";
447
+ break;
448
+ }
449
+ if (nextLastId === fromId) {
450
+ stats.stopReason = "Cursor stopped advancing";
451
+ coverageComplete = false;
452
+ break;
453
+ }
454
+ if (rows.length === 0) {
455
+ stats.stopReason = "Empty page returned before terminal cursor";
456
+ coverageComplete = false;
457
+ break;
458
+ }
459
+ fromId = nextLastId;
460
+ await waitBetweenRequests(config.delayMs);
461
+ }
462
+ if (page >= config.maxPages && stats.stopReason == null) {
463
+ coverageComplete = false;
464
+ stats.stopReason = `Reached max-pages=${config.maxPages}`;
465
+ }
466
+ stats.coverageComplete = coverageComplete;
467
+ stats.elapsedMs = Date.now() - startedAtMs;
468
+ return buildStrategyReport(stats);
469
+ }
470
+ function chunkArray(values, size) {
471
+ const chunks = [];
472
+ if (size <= 0) {
473
+ return chunks;
474
+ }
475
+ for (let index = 0; index < values.length; index += size) {
476
+ chunks.push(values.slice(index, index + size));
477
+ }
478
+ return chunks;
479
+ }
480
+ async function profileOrderIdsStrategy(api, config, startedAtMs) {
481
+ const stats = createStrategyStats();
482
+ const effectiveBatchSize = clampMercadoLibreBillingOrderIdsBatchSize(config.batchSize);
483
+ const chunks = chunkArray(config.orderIds, effectiveBatchSize);
484
+ const limitedChunks = chunks.slice(0, config.maxPages);
485
+ if (effectiveBatchSize !== config.batchSize) {
486
+ console.log(
487
+ JSON.stringify({
488
+ event: "order_ids_batch_size_capped",
489
+ requested: config.batchSize,
490
+ effective: effectiveBatchSize,
491
+ cap: MERCADO_LIBRE_BILLING_ORDER_IDS_MAX_PER_REQUEST
492
+ })
493
+ );
494
+ }
495
+ for (let index = 0; index < limitedChunks.length; index += 1) {
496
+ const chunk = limitedChunks[index];
497
+ stats.requestsAttempted += 1;
498
+ const groupPath = "ML";
499
+ const endpoint = buildMercadoLibreBillingOrderDetailsPath();
500
+ const params = {
501
+ document_type: config.documentType,
502
+ order_ids: chunk.join(",")
503
+ };
504
+ const result = await requestWithRetries(
505
+ api,
506
+ {
507
+ strategy: "order-ids",
508
+ step: `batch-${index + 1}`,
509
+ endpoint,
510
+ scope: "ml",
511
+ groupPath,
512
+ params
513
+ },
514
+ config.maxRetries,
515
+ config.delayMs
516
+ );
517
+ stats.requestLogs.push(...result.logs);
518
+ stats.transportAttempts += result.logs.length;
519
+ stats.rateLimit429Count += result.logs.filter((log) => log.status === 429).length;
520
+ if (!result.ok) {
521
+ stats.requestsFailed += 1;
522
+ stats.stopReason = `Stopped on order batch ${index + 1} after a failed request`;
523
+ stats.coverageComplete = false;
524
+ break;
525
+ }
526
+ stats.requestsSucceeded += 1;
527
+ const lastLog = result.logs[result.logs.length - 1];
528
+ stats.totalLatencyMs += lastLog?.durationMs ?? 0;
529
+ const responseRecord = asRecord(result.data);
530
+ const rows = Array.isArray(responseRecord.results) ? responseRecord.results.filter((entry) => entry != null && typeof entry === "object" && !Array.isArray(entry)) : [];
531
+ summarizeResponseRows("order-ids", rows, stats);
532
+ const nextLastId = normalizeCursor(responseRecord.last_id);
533
+ if (nextLastId) {
534
+ stats.lastIdsSeen.push(nextLastId);
535
+ }
536
+ await waitBetweenRequests(config.delayMs);
537
+ }
538
+ if (limitedChunks.length < chunks.length && stats.stopReason == null) {
539
+ stats.stopReason = `Reached max-pages=${config.maxPages} before exhausting all order-id batches`;
540
+ stats.coverageComplete = false;
541
+ } else if (stats.coverageComplete == null) {
542
+ stats.coverageComplete = stats.requestsFailed === 0;
543
+ }
544
+ stats.elapsedMs = Date.now() - startedAtMs;
545
+ return buildStrategyReport(stats);
546
+ }
547
+ async function main() {
548
+ loadLocalEnv();
549
+ const config = parseConfig();
550
+ const startedAt = nowIso();
551
+ const startedAtMs = Date.now();
552
+ const { api } = await getMercadoLibreApiContext(config.profileId);
553
+ const requestLogs = [];
554
+ let periodReport;
555
+ let orderIdsReport;
556
+ console.log(
557
+ JSON.stringify({
558
+ event: "run_start",
559
+ startedAt,
560
+ config: {
561
+ profileId: config.profileId,
562
+ periodKey: config.periodKey,
563
+ documentType: config.documentType,
564
+ mode: config.mode,
565
+ scope: config.scope,
566
+ pageLimit: config.pageLimit,
567
+ maxPages: config.maxPages,
568
+ batchSize: config.batchSize,
569
+ orderIdsBatchSizeCap: MERCADO_LIBRE_BILLING_ORDER_IDS_MAX_PER_REQUEST,
570
+ delayMs: config.delayMs,
571
+ maxRetries: config.maxRetries,
572
+ orderIdsCount: config.orderIds.length,
573
+ outputPath: config.outputPath
574
+ }
575
+ })
576
+ );
577
+ if (config.mode === "period" || config.mode === "both") {
578
+ periodReport = await profilePeriodStrategy(api, config, startedAtMs);
579
+ requestLogs.push(...periodReport.requestLogs);
580
+ }
581
+ if (config.mode === "order-ids" || config.mode === "both") {
582
+ orderIdsReport = await profileOrderIdsStrategy(api, config, startedAtMs);
583
+ requestLogs.push(...orderIdsReport.requestLogs);
584
+ }
585
+ const endedAt = nowIso();
586
+ const durationMs = Date.now() - startedAtMs;
587
+ const summary = {
588
+ requestsAttempted: (periodReport?.requestsAttempted ?? 0) + (orderIdsReport?.requestsAttempted ?? 0),
589
+ requestsSucceeded: (periodReport?.requestsSucceeded ?? 0) + (orderIdsReport?.requestsSucceeded ?? 0),
590
+ requestsFailed: (periodReport?.requestsFailed ?? 0) + (orderIdsReport?.requestsFailed ?? 0),
591
+ transportAttempts: (periodReport?.transportAttempts ?? 0) + (orderIdsReport?.transportAttempts ?? 0),
592
+ rateLimit429Count: (periodReport?.rateLimit429Count ?? 0) + (orderIdsReport?.rateLimit429Count ?? 0),
593
+ rowsRecovered: (periodReport?.rowsRecovered ?? 0) + (orderIdsReport?.rowsRecovered ?? 0),
594
+ averageLatencyMs: requestLogs.length > 0 ? Math.round(requestLogs.reduce((sum, log) => sum + log.durationMs, 0) / requestLogs.length) : 0,
595
+ estimatedThroughputRowsPerSec: durationMs > 0 ? Math.round(((periodReport?.rowsRecovered ?? 0) + (orderIdsReport?.rowsRecovered ?? 0)) / (durationMs / 1e3) * 100) / 100 : 0,
596
+ recommendationHint: recommendStrategy(periodReport, orderIdsReport)
597
+ };
598
+ const artifact = {
599
+ run: {
600
+ startedAt,
601
+ endedAt,
602
+ durationMs,
603
+ config
604
+ },
605
+ summary,
606
+ requestLogs
607
+ };
608
+ if (periodReport) {
609
+ artifact.period = {
610
+ strategy: "period",
611
+ stats: periodReport
612
+ };
613
+ }
614
+ if (orderIdsReport) {
615
+ artifact.orderIds = {
616
+ strategy: "order-ids",
617
+ stats: orderIdsReport
618
+ };
619
+ }
620
+ await mkdir("/tmp/opencode", { recursive: true });
621
+ await writeFile(config.outputPath, `${JSON.stringify(artifact, null, 2)}
622
+ `, "utf8");
623
+ console.log(JSON.stringify({ event: "artifact_written", path: config.outputPath, durationMs }));
624
+ console.log(JSON.stringify(artifact, null, 2));
625
+ }
626
+ main().catch((error) => {
627
+ console.error("Mercado Libre billing profiler failed");
628
+ console.error(formatMercadoLibreError(error, "Mercado Libre billing profiler failed"));
629
+ if (error instanceof Error && error.stack) {
630
+ console.error(error.stack);
631
+ }
632
+ process.exitCode = 1;
633
+ });
634
+ //# sourceMappingURL=profile-mercadolibre-billing.js.map