@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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/mcp-use.json +6 -3
- package/dist/scripts/profile-mercadolibre-billing.js +634 -0
- package/dist/scripts/profile-mercadolibre-billing.js.map +7 -0
- package/dist/src/config/__tests__/tiendanube-profile-store.test.js +50 -0
- package/dist/src/config/__tests__/tiendanube-profile-store.test.js.map +7 -0
- package/dist/src/config/tiendanube-profile-store.js +31 -0
- package/dist/src/config/tiendanube-profile-store.js.map +7 -0
- package/dist/src/google-ads/report-utils.js +78 -0
- package/dist/src/google-ads/report-utils.js.map +7 -0
- package/dist/src/search-console/search-console-utils.js +275 -0
- package/dist/src/search-console/search-console-utils.js.map +7 -0
- package/dist/src/services/mercadolibre/__tests__/mercadolibre-sales-report-ventas.test.js +358 -0
- package/dist/src/services/mercadolibre/__tests__/mercadolibre-sales-report-ventas.test.js.map +7 -0
- package/dist/src/services/mercadolibre/__tests__/mercadolibre-sales-report.test.js +96 -0
- package/dist/src/services/mercadolibre/__tests__/mercadolibre-sales-report.test.js.map +7 -0
- package/dist/src/services/mercadolibre/mercadolibre-claim-return-lookup.js +15 -0
- package/dist/src/services/mercadolibre/mercadolibre-claim-return-lookup.js.map +7 -0
- package/dist/src/services/mercadolibre/mercadolibre-observability.js +12 -0
- package/dist/src/services/mercadolibre/mercadolibre-observability.js.map +7 -0
- package/dist/src/services/mercadolibre/mercadolibre-sale-reference.js +7 -0
- package/dist/src/services/mercadolibre/mercadolibre-sale-reference.js.map +7 -0
- package/dist/src/services/mercadolibre/mercadolibre-sales-report.js +553 -0
- package/dist/src/services/mercadolibre/mercadolibre-sales-report.js.map +7 -0
- package/dist/src/services/vtex/__tests__/vtex-catalog-write-batch.test.js +165 -0
- package/dist/src/services/vtex/__tests__/vtex-catalog-write-batch.test.js.map +7 -0
- package/dist/src/tools/config/__tests__/list-profiles.test.js +144 -1
- package/dist/src/tools/config/__tests__/list-profiles.test.js.map +2 -2
- package/dist/src/tools/config/list-profiles.js +50 -6
- package/dist/src/tools/config/list-profiles.js.map +2 -2
- package/dist/src/tools/mercadolibre/__tests__/get-sales-report-foundation.test.js +417 -0
- package/dist/src/tools/mercadolibre/__tests__/get-sales-report-foundation.test.js.map +7 -0
- package/dist/src/tools/mercadolibre/get-sales-report.js +474 -0
- package/dist/src/tools/mercadolibre/get-sales-report.js.map +7 -0
- package/dist/src/tools/vtex/__tests__/catalog-admin-batch.test.js +233 -0
- package/dist/src/tools/vtex/__tests__/catalog-admin-batch.test.js.map +7 -0
- package/dist/src/tools/vtex/catalog-admin-batch.js +298 -0
- package/dist/src/tools/vtex/catalog-admin-batch.js.map +7 -0
- package/dist/tests/meli/meli-sales-report-buyers-mcp-e2e.test.js +137 -0
- package/dist/tests/meli/meli-sales-report-buyers-mcp-e2e.test.js.map +7 -0
- package/dist/tests/meli/meli-sales-report-mcp-e2e-server.js +24 -0
- package/dist/tests/meli/meli-sales-report-mcp-e2e-server.js.map +7 -0
- package/dist/tests/meli/meli-sales-report-mcp-e2e.test.js +207 -0
- package/dist/tests/meli/meli-sales-report-mcp-e2e.test.js.map +7 -0
- package/dist/tests/meli/meli-sales-report-public-mcp-e2e-server.js +10 -0
- package/dist/tests/meli/meli-sales-report-public-mcp-e2e-server.js.map +7 -0
- package/dist/tests/meli/meli-sales-report-public-registration.test.js +98 -0
- package/dist/tests/meli/meli-sales-report-public-registration.test.js.map +7 -0
- package/dist/tests/smoke/process-harness.js +89 -0
- package/dist/tests/smoke/process-harness.js.map +7 -0
- package/dist/tests/smoke/process-harness.test.js +39 -0
- package/dist/tests/smoke/process-harness.test.js.map +7 -0
- package/package.json +1 -1
package/dist/mcp-use.json
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"includeInspector": false,
|
|
3
|
-
"buildTime": "2026-
|
|
4
|
-
"buildId": "
|
|
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
|