@yoryoboy/bi-mcp 1.7.0 → 1.9.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 (31) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/index.js +27 -5
  3. package/dist/index.js.map +2 -2
  4. package/dist/mcp-use.json +6 -3
  5. package/dist/src/services/mercadolibre/mercadolibre-billing.js +77 -0
  6. package/dist/src/services/mercadolibre/mercadolibre-billing.js.map +7 -0
  7. package/dist/src/services/mercadolibre/mercadolibre-items.js +29 -2
  8. package/dist/src/services/mercadolibre/mercadolibre-items.js.map +2 -2
  9. package/dist/src/services/mercadolibre/mercadolibre-orders.js +18 -0
  10. package/dist/src/services/mercadolibre/mercadolibre-orders.js.map +2 -2
  11. package/dist/src/tools/mercadolibre/estimate-listing-fee.js +26 -34
  12. package/dist/src/tools/mercadolibre/estimate-listing-fee.js.map +2 -2
  13. package/dist/src/tools/mercadolibre/estimate-product-profitability.js +546 -0
  14. package/dist/src/tools/mercadolibre/estimate-product-profitability.js.map +7 -0
  15. package/dist/src/tools/mercadolibre/get-item-details.js +3 -25
  16. package/dist/src/tools/mercadolibre/get-item-details.js.map +2 -2
  17. package/dist/src/tools/mercadolibre/get-sales-by-item.js +129 -14
  18. package/dist/src/tools/mercadolibre/get-sales-by-item.js.map +2 -2
  19. package/dist/src/tools/mercadolibre/get-sales-trend.js +137 -14
  20. package/dist/src/tools/mercadolibre/get-sales-trend.js.map +2 -2
  21. package/dist/src/tools/mercadolibre/get-shipping-summary.js +450 -57
  22. package/dist/src/tools/mercadolibre/get-shipping-summary.js.map +2 -2
  23. package/dist/src/tools/mercadolibre/get-site-categories-and-listing-types.js +50 -0
  24. package/dist/src/tools/mercadolibre/get-site-categories-and-listing-types.js.map +7 -0
  25. package/dist/src/tools/mercadolibre/index.js +2 -0
  26. package/dist/src/tools/mercadolibre/index.js.map +2 -2
  27. package/dist/src/tools/mercadolibre/listing-fee-helpers.js +265 -0
  28. package/dist/src/tools/mercadolibre/listing-fee-helpers.js.map +7 -0
  29. package/dist/src/tools/mercadolibre/search-items.js +207 -52
  30. package/dist/src/tools/mercadolibre/search-items.js.map +2 -2
  31. package/package.json +1 -1
@@ -1,10 +1,14 @@
1
1
  import { error, object } from "mcp-use/server";
2
2
  import { z } from "zod";
3
- import { formatMercadoLibreError } from "../../services/mercadolibre/mercadolibre-api.js";
4
3
  import {
5
- getMercadoLibreShipment,
6
- getMercadoLibreShipmentCosts,
7
- searchMercadoLibreOrders
4
+ formatMercadoLibreError,
5
+ normalizeMercadoLibrePaging
6
+ } from "../../services/mercadolibre/mercadolibre-api.js";
7
+ import {
8
+ getMercadoLibreShipmentBatch,
9
+ getMercadoLibreShipmentCostsBatch,
10
+ searchMercadoLibreOrders,
11
+ searchMercadoLibreOrdersBatch
8
12
  } from "../../services/mercadolibre/mercadolibre-orders.js";
9
13
  import { stripNulls } from "../../utils/strip-payload.js";
10
14
  import {
@@ -12,19 +16,228 @@ import {
12
16
  asRecord,
13
17
  currencyBucket,
14
18
  mercadoLibreDateRegex,
19
+ normalizeScalarString,
15
20
  normalizeString,
16
21
  roundMoney,
17
22
  toNumber
18
23
  } from "./helpers.js";
19
24
  import { resolveMercadoLibreProfileOrSelection } from "./profile-resolution.js";
20
25
  import { mercadolibreProfileIdSchemaField } from "./write-helpers.js";
26
+ const PAGE_FETCH_CONCURRENCY = 15;
27
+ const PAGE_FETCH_MAX_RETRIES = 2;
28
+ const SHIPPING_DETAIL_MAX_PAGES_PER_CALL = 15;
29
+ const DEFAULT_CALCULATIONS_PAGE_LIMIT = 50;
30
+ const DEFAULT_SHIPPING_DETAIL_PAGE_LIMIT = 50;
31
+ const BUSINESS_TIME_ZONE = "America/Argentina/Buenos_Aires";
32
+ const TOP_LIMIT = 10;
33
+ const shipmentsSchema = [
34
+ "order_id",
35
+ "shipment_id",
36
+ "status",
37
+ "substatus",
38
+ "logistic_type",
39
+ "mode",
40
+ "currency_id",
41
+ "cost"
42
+ ];
43
+ function safeRate(numerator, denominator) {
44
+ if (!Number.isFinite(denominator) || denominator <= 0) {
45
+ return 0;
46
+ }
47
+ return Number((numerator / denominator).toFixed(4));
48
+ }
49
+ function topBreakdownFromMap(counts, total, limit = TOP_LIMIT) {
50
+ const sorted = Array.from(counts.entries()).filter(([, count]) => count > 0).sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]));
51
+ const entries = sorted.slice(0, limit).map(([key, orders]) => ({
52
+ key,
53
+ orders,
54
+ rate: safeRate(orders, total)
55
+ }));
56
+ const otherOrders = sorted.slice(limit).reduce((sum, [, count]) => sum + count, 0);
57
+ return stripNulls({
58
+ entries,
59
+ other_orders: otherOrders > 0 ? otherOrders : void 0,
60
+ other_rate: otherOrders > 0 ? safeRate(otherOrders, total) : void 0
61
+ });
62
+ }
63
+ function incrementCount(map, key) {
64
+ if (!key) {
65
+ return;
66
+ }
67
+ map.set(key, (map.get(key) ?? 0) + 1);
68
+ }
69
+ function topSingleValue(counts) {
70
+ return Array.from(counts.entries()).filter(([key]) => key).sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))[0]?.[0] ?? "";
71
+ }
72
+ function incrementCurrency(map, currencyId, amount) {
73
+ map[currencyId] = roundMoney((map[currencyId] ?? 0) + amount);
74
+ }
75
+ function sortedCurrencyTotals(input) {
76
+ return Object.fromEntries(
77
+ Object.entries(input).filter(([, amount]) => amount !== 0).sort(([left], [right]) => left.localeCompare(right)).map(([currencyId, amount]) => [currencyId, roundMoney(amount)])
78
+ );
79
+ }
80
+ function formatDateInTimeZone(dateValue, timeZone) {
81
+ const formatter = new Intl.DateTimeFormat("en-CA", {
82
+ timeZone,
83
+ year: "numeric",
84
+ month: "2-digit",
85
+ day: "2-digit"
86
+ });
87
+ const parts = formatter.formatToParts(dateValue);
88
+ const year = parts.find((part) => part.type === "year")?.value;
89
+ const month = parts.find((part) => part.type === "month")?.value;
90
+ const day = parts.find((part) => part.type === "day")?.value;
91
+ if (!year || !month || !day) {
92
+ return "";
93
+ }
94
+ return `${year}-${month}-${day}`;
95
+ }
96
+ function getLocalDateBucket(createdAt, timeZone) {
97
+ if (!createdAt) {
98
+ return "";
99
+ }
100
+ const date = new Date(createdAt);
101
+ if (Number.isNaN(date.getTime())) {
102
+ return "";
103
+ }
104
+ return formatDateInTimeZone(date, timeZone);
105
+ }
106
+ function buildShippingSummaryMetadata(params) {
107
+ const requestedWindowEndOffset = params.effectiveOffset + params.effectiveLimit * params.pagesRequested;
108
+ const nextOffset = params.responseMode === "shipments_chunk" ? requestedWindowEndOffset : params.effectiveOffset + params.returned;
109
+ const hasMoreOrders = nextOffset < params.total;
110
+ const hasFailures = params.pagesFailed > 0 || params.shipmentFailures > 0;
111
+ const universeFullyFetched = params.responseMode === "calculations" && !hasFailures && params.returned >= params.total;
112
+ let continuation = "No more pages for this query.";
113
+ if (params.pagesFailed > 0) {
114
+ const offsets = params.failedOffsets.slice(0, 5).join(", ");
115
+ const moreOffsets = params.failedOffsets.length > 5 ? ` y ${params.failedOffsets.length - 5} offsets adicionales` : "";
116
+ continuation = params.responseMode === "shipments_chunk" ? `Se recuperaron paginas parciales. Reintentar primero la misma tool con responseMode="shipments_chunk", offset=${params.failedOffsets[0]} y limit=${params.effectiveLimit}. Offsets fallidos: ${offsets}${moreOffsets}. Luego continuar con offset=${nextOffset} si hace falta.` : `Calculos parciales por paginas fallidas. Reintentar la misma tool con responseMode="calculations". Offsets fallidos: ${offsets}${moreOffsets}.`;
117
+ } else if (params.shipmentFailures > 0) {
118
+ continuation = params.responseMode === "shipments_chunk" ? `Se recuperaron shipments parciales. Reintentar la misma tool con responseMode="shipments_chunk", offset=${params.effectiveOffset} y limit=${params.effectiveLimit}${hasMoreOrders ? `. Luego continuar con offset=${nextOffset} si hace falta.` : "."}` : 'Calculos parciales por fallas al consultar shipments o costos. Reintentar la misma tool con responseMode="calculations".';
119
+ } else if (params.responseMode === "shipments_chunk" && hasMoreOrders) {
120
+ continuation = `Para continuar trayendo shipments, volver a invocar meli_get_shipping_summary con los mismos filtros, responseMode="shipments_chunk", offset=${nextOffset} y limit=${params.effectiveLimit}. Repetir hasta has_more=false.`;
121
+ } else if (params.responseMode === "calculations") {
122
+ continuation = 'Calculos completos. Si el usuario pide detalle de shipments, invocar la misma tool con responseMode="shipments_chunk", offset=0 y limit=50.';
123
+ }
124
+ return stripNulls({
125
+ total: params.total,
126
+ limit: params.effectiveLimit,
127
+ offset: params.effectiveOffset,
128
+ returned: params.returned,
129
+ has_more: params.responseMode === "shipments_chunk" ? hasMoreOrders || hasFailures : hasFailures,
130
+ next_offset: params.responseMode === "shipments_chunk" && hasMoreOrders ? nextOffset : void 0,
131
+ continuation,
132
+ fetch_mode: params.responseMode === "shipments_chunk" ? "shipments_chunk" : "full_calculations",
133
+ pages_per_call_limit: params.responseMode === "shipments_chunk" ? SHIPPING_DETAIL_MAX_PAGES_PER_CALL : void 0,
134
+ pages_requested: params.pagesRequested,
135
+ pages_succeeded: params.pagesSucceeded,
136
+ pages_failed: params.pagesFailed,
137
+ shipment_failures: params.shipmentFailures,
138
+ universe_fully_fetched: universeFullyFetched
139
+ });
140
+ }
141
+ function extractShipmentCost(costs) {
142
+ return toNumber(asRecord(costs.senders).cost) || toNumber(asRecord(costs.receiver).cost) || toNumber(costs.gross_amount) || toNumber(costs.cost);
143
+ }
144
+ function formatCompactShipmentRow(params) {
145
+ return [
146
+ params.orderId,
147
+ params.shipmentId,
148
+ params.status,
149
+ params.substatus,
150
+ params.logisticType,
151
+ params.mode,
152
+ params.currencyId,
153
+ params.cost
154
+ ];
155
+ }
156
+ function buildShippingCalculations(params) {
157
+ const costsByCurrency = Object.fromEntries(
158
+ Object.entries(params.costsByCurrency).sort(([left], [right]) => left.localeCompare(right)).map(([currencyId, bucket]) => [
159
+ currencyId,
160
+ {
161
+ total_shipping_cost: roundMoney(bucket.total_cost),
162
+ shipments: bucket.shipments,
163
+ shipments_with_cost: bucket.shipments_with_cost,
164
+ zero_cost_shipments: bucket.zero_cost_shipments,
165
+ avg_cost_per_shipment: roundMoney(bucket.total_cost / Math.max(1, bucket.shipments)),
166
+ avg_cost_per_shipment_with_cost: roundMoney(
167
+ bucket.total_cost / Math.max(1, bucket.shipments_with_cost)
168
+ ),
169
+ shipping_cost_rate_by_shipment: safeRate(bucket.shipments_with_cost, bucket.shipments)
170
+ }
171
+ ])
172
+ );
173
+ const dailyTrend = Array.from(params.dailyMetrics.entries()).sort(([left], [right]) => left.localeCompare(right)).map(
174
+ ([, metric]) => stripNulls({
175
+ date: metric.date,
176
+ shipments: metric.shipments,
177
+ delivered_shipments: metric.delivered_shipments,
178
+ cancelled_shipments: metric.cancelled_shipments,
179
+ not_delivered_shipments: metric.not_delivered_shipments,
180
+ shipping_cost_by_currency: sortedCurrencyTotals(metric.shipping_cost_by_currency)
181
+ })
182
+ );
183
+ return {
184
+ overview: {
185
+ orders_total: params.ordersTotal,
186
+ orders_with_shipping: params.ordersWithShipping,
187
+ shipments_total: params.shipmentsInspected,
188
+ shipments_requested: params.shipmentsRequested,
189
+ shipping_attachment_rate: safeRate(params.ordersWithShipping, params.ordersTotal),
190
+ delivered_shipments: params.deliveredShipments,
191
+ cancelled_shipments: params.cancelledShipments,
192
+ not_delivered_shipments: params.notDeliveredShipments
193
+ },
194
+ delivery_health: {
195
+ status_breakdown: topBreakdownFromMap(params.statusCounts, Math.max(1, params.shipmentsInspected)),
196
+ substatus_breakdown: topBreakdownFromMap(params.substatusCounts, Math.max(1, params.shipmentsInspected)),
197
+ delivered_rate: safeRate(params.deliveredShipments, params.shipmentsInspected),
198
+ cancelled_rate: safeRate(params.cancelledShipments, params.shipmentsInspected),
199
+ not_delivered_rate: safeRate(params.notDeliveredShipments, params.shipmentsInspected)
200
+ },
201
+ costs_by_currency: costsByCurrency,
202
+ logistics_mix: {
203
+ logistic_type_breakdown: topBreakdownFromMap(
204
+ params.logisticTypeCounts,
205
+ Math.max(1, params.shipmentsInspected)
206
+ ),
207
+ mode_breakdown: topBreakdownFromMap(params.modeCounts, Math.max(1, params.shipmentsInspected)),
208
+ top_logistic_type_by_shipments: topSingleValue(params.logisticTypeCounts),
209
+ top_mode_by_shipments: topSingleValue(params.modeCounts)
210
+ },
211
+ daily_trend: dailyTrend,
212
+ coverage: {
213
+ fetch_mode: params.metadata.fetch_mode,
214
+ universe_fully_fetched: params.metadata.universe_fully_fetched,
215
+ orders_returned: params.metadata.returned,
216
+ shipments_requested: params.shipmentsRequested,
217
+ shipments_inspected: params.shipmentsInspected,
218
+ pages_requested: params.metadata.pages_requested,
219
+ pages_succeeded: params.metadata.pages_succeeded,
220
+ pages_failed: params.metadata.pages_failed,
221
+ failed_pages_count: params.failedPages.length,
222
+ shipment_failures_count: params.shipmentFailures.length,
223
+ failed_pages: params.failedPages
224
+ }
225
+ };
226
+ }
21
227
  const meliGetShippingSummarySchema = z.object({
22
228
  profileId: mercadolibreProfileIdSchemaField,
23
229
  startDate: z.string().regex(mercadoLibreDateRegex).describe("Start date in YYYY-MM-DD format."),
24
230
  endDate: z.string().regex(mercadoLibreDateRegex).describe("End date in YYYY-MM-DD format."),
25
231
  status: z.string().trim().min(1).optional().describe("Optional order status filter."),
26
- limit: z.number().int().min(1).max(30).optional().describe("Orders page size to inspect for shipping data."),
27
- offset: z.number().int().min(0).max(5e3).optional().describe("Orders offset to inspect.")
232
+ responseMode: z.enum(["calculations", "shipments_chunk"]).optional().default("calculations").describe(
233
+ "Response mode. calculations (default) fetches all matching orders internally and returns only aggregate shipping KPIs. shipments_chunk returns compact shipment rows for up to 15 MercadoLibre pages and includes continuation metadata."
234
+ ),
235
+ limit: z.number().int().min(1).max(50).optional().describe(
236
+ 'MercadoLibre page size for responseMode="shipments_chunk" (1-50). Defaults to 50.'
237
+ ),
238
+ offset: z.number().int().min(0).max(5e3).optional().describe(
239
+ 'Starting offset for responseMode="shipments_chunk". Use metadata.next_offset to continue.'
240
+ )
28
241
  });
29
242
  async function meliGetShippingSummaryHandler(params) {
30
243
  try {
@@ -33,75 +246,255 @@ async function meliGetShippingSummaryHandler(params) {
33
246
  return profileResolution.response;
34
247
  }
35
248
  const profileId = profileResolution.value.profileId;
36
- const ordersResponse = await searchMercadoLibreOrders(profileId, {
249
+ const responseMode = params.responseMode ?? "calculations";
250
+ const requestedLimit = responseMode === "shipments_chunk" ? params.limit ?? DEFAULT_SHIPPING_DETAIL_PAGE_LIMIT : DEFAULT_CALCULATIONS_PAGE_LIMIT;
251
+ const requestedOffset = responseMode === "shipments_chunk" ? params.offset ?? 0 : 0;
252
+ const maxPagesForResponse = responseMode === "shipments_chunk" ? SHIPPING_DETAIL_MAX_PAGES_PER_CALL : Infinity;
253
+ const response = await searchMercadoLibreOrders(profileId, {
37
254
  seller: "",
38
255
  from: params.startDate,
39
256
  to: params.endDate,
40
257
  status: params.status,
41
- limit: params.limit ?? 20,
42
- offset: params.offset ?? 0
258
+ limit: requestedLimit,
259
+ offset: requestedOffset
43
260
  });
44
- const statusCounts = {};
45
- const substatusCounts = {};
261
+ const paging = normalizeMercadoLibrePaging(asRecord(response.paging));
262
+ const baseOrders = asArray(response.results);
263
+ const effectiveLimit = paging.limit || requestedLimit || baseOrders.length;
264
+ const effectiveOffset = paging.offset || requestedOffset || 0;
265
+ const allOrders = [...baseOrders];
266
+ const failedPages = [];
267
+ let pagesRequested = 1;
268
+ let pagesSucceeded = 1;
269
+ if (effectiveLimit > 0 && paging.total > baseOrders.length) {
270
+ const nextOffsets = [];
271
+ const maxPagesRemaining = Math.max(0, maxPagesForResponse - 1);
272
+ let pagesAdded = 0;
273
+ for (let nextOffset = effectiveOffset + effectiveLimit; nextOffset < paging.total; nextOffset += effectiveLimit) {
274
+ if (responseMode === "shipments_chunk" && pagesAdded >= maxPagesRemaining) {
275
+ break;
276
+ }
277
+ nextOffsets.push(nextOffset);
278
+ pagesAdded += 1;
279
+ }
280
+ pagesRequested += nextOffsets.length;
281
+ if (nextOffsets.length > 0) {
282
+ const batch = await searchMercadoLibreOrdersBatch(
283
+ profileId,
284
+ {
285
+ seller: "",
286
+ from: params.startDate,
287
+ to: params.endDate,
288
+ status: params.status,
289
+ limit: effectiveLimit
290
+ },
291
+ nextOffsets,
292
+ {
293
+ maxConcurrency: PAGE_FETCH_CONCURRENCY,
294
+ maxRetries: PAGE_FETCH_MAX_RETRIES
295
+ }
296
+ );
297
+ pagesSucceeded += batch.successful.length;
298
+ batch.successful.forEach((entry) => {
299
+ allOrders.push(...asArray(entry.document.results));
300
+ });
301
+ batch.failed.forEach((failure) => {
302
+ const failedOffset = Number(failure.id);
303
+ failedPages.push({
304
+ offset: failedOffset,
305
+ limit: effectiveLimit,
306
+ page_number: Math.floor(failedOffset / effectiveLimit) + 1,
307
+ message: failure.message,
308
+ status_code: failure.statusCode,
309
+ attempts: failure.attempts,
310
+ retryable: failure.retryable
311
+ });
312
+ });
313
+ }
314
+ }
315
+ const ordersWithShipping = allOrders.reduce((count, order) => {
316
+ return normalizeScalarString(asRecord(order.shipping).id) ? count + 1 : count;
317
+ }, 0);
318
+ const orderByShipmentId = /* @__PURE__ */ new Map();
319
+ for (const order of allOrders) {
320
+ const shipmentId = normalizeScalarString(asRecord(order.shipping).id);
321
+ if (shipmentId && !orderByShipmentId.has(shipmentId)) {
322
+ orderByShipmentId.set(shipmentId, order);
323
+ }
324
+ }
325
+ const shipmentIds = [...orderByShipmentId.keys()];
326
+ const statusCounts = /* @__PURE__ */ new Map();
327
+ const substatusCounts = /* @__PURE__ */ new Map();
328
+ const logisticTypeCounts = /* @__PURE__ */ new Map();
329
+ const modeCounts = /* @__PURE__ */ new Map();
46
330
  const costsByCurrency = {};
47
- const partialFailures = [];
331
+ const dailyMetrics = /* @__PURE__ */ new Map();
332
+ const failures = [];
48
333
  const shipmentRows = [];
49
- for (const order of asArray(ordersResponse.results)) {
50
- const shipping = asRecord(order.shipping);
51
- const shipmentId = normalizeString(shipping.id);
52
- if (!shipmentId) {
53
- continue;
54
- }
55
- try {
56
- const [shipment, costs] = await Promise.all([
57
- getMercadoLibreShipment(profileId, shipmentId),
58
- getMercadoLibreShipmentCosts(profileId, shipmentId)
59
- ]);
334
+ let shipmentsInspected = 0;
335
+ let deliveredShipments = 0;
336
+ let cancelledShipments = 0;
337
+ let notDeliveredShipments = 0;
338
+ if (shipmentIds.length > 0) {
339
+ const [shipmentsBatch, costsBatch] = await Promise.all([
340
+ getMercadoLibreShipmentBatch(profileId, shipmentIds, {
341
+ maxConcurrency: PAGE_FETCH_CONCURRENCY,
342
+ maxRetries: PAGE_FETCH_MAX_RETRIES
343
+ }),
344
+ getMercadoLibreShipmentCostsBatch(profileId, shipmentIds, {
345
+ maxConcurrency: PAGE_FETCH_CONCURRENCY,
346
+ maxRetries: PAGE_FETCH_MAX_RETRIES
347
+ })
348
+ ]);
349
+ const shipmentById = new Map(
350
+ shipmentsBatch.successful.map((entry) => [entry.id, entry.document])
351
+ );
352
+ const costsByShipmentId = new Map(
353
+ costsBatch.successful.map((entry) => [entry.id, entry.document])
354
+ );
355
+ shipmentsBatch.failed.forEach((failure) => {
356
+ failures.push({
357
+ order_id: normalizeScalarString(orderByShipmentId.get(failure.id)?.id),
358
+ shipment_id: failure.id,
359
+ stage: "shipment",
360
+ message: failure.message,
361
+ status_code: failure.statusCode,
362
+ attempts: failure.attempts,
363
+ retryable: failure.retryable
364
+ });
365
+ });
366
+ costsBatch.failed.forEach((failure) => {
367
+ failures.push({
368
+ order_id: normalizeScalarString(orderByShipmentId.get(failure.id)?.id),
369
+ shipment_id: failure.id,
370
+ stage: "costs",
371
+ message: failure.message,
372
+ status_code: failure.statusCode,
373
+ attempts: failure.attempts,
374
+ retryable: failure.retryable
375
+ });
376
+ });
377
+ for (const shipmentId of shipmentIds) {
378
+ const shipment = shipmentById.get(shipmentId);
379
+ const costs = costsByShipmentId.get(shipmentId);
380
+ if (!shipment || !costs) {
381
+ continue;
382
+ }
383
+ const order = orderByShipmentId.get(shipmentId);
60
384
  const shipmentStatus = normalizeString(shipment.status, "unknown");
61
385
  const shipmentSubstatus = normalizeString(shipment.substatus, "unknown");
62
- statusCounts[shipmentStatus] = (statusCounts[shipmentStatus] ?? 0) + 1;
63
- substatusCounts[shipmentSubstatus] = (substatusCounts[shipmentSubstatus] ?? 0) + 1;
64
- const currencyId = normalizeString(order.currency_id, "UNKNOWN");
65
- const bucket = currencyBucket(costsByCurrency, currencyId, () => ({ shipments: 0, total_cost: 0 }));
386
+ const logisticType = normalizeString(shipment.logistic_type);
387
+ const mode = normalizeString(shipment.mode);
388
+ const currencyId = normalizeString(order?.currency_id, "UNKNOWN");
389
+ const shipmentCost = roundMoney(extractShipmentCost(costs));
390
+ const dayBucket = getLocalDateBucket(normalizeString(order?.date_created), BUSINESS_TIME_ZONE);
391
+ shipmentsInspected += 1;
392
+ incrementCount(statusCounts, shipmentStatus);
393
+ incrementCount(substatusCounts, shipmentSubstatus);
394
+ incrementCount(logisticTypeCounts, logisticType || "unknown");
395
+ incrementCount(modeCounts, mode || "unknown");
396
+ if (shipmentStatus === "delivered") {
397
+ deliveredShipments += 1;
398
+ }
399
+ if (shipmentStatus === "cancelled") {
400
+ cancelledShipments += 1;
401
+ }
402
+ if (shipmentStatus === "not_delivered") {
403
+ notDeliveredShipments += 1;
404
+ }
405
+ const bucket = currencyBucket(costsByCurrency, currencyId, () => ({
406
+ shipments: 0,
407
+ shipments_with_cost: 0,
408
+ zero_cost_shipments: 0,
409
+ total_cost: 0
410
+ }));
66
411
  bucket.shipments += 1;
67
- bucket.total_cost += toNumber(asRecord(costs.senders).cost) || toNumber(asRecord(costs.receiver).cost) || toNumber(costs.gross_amount) || toNumber(costs.cost);
68
- shipmentRows.push(
69
- stripNulls({
70
- order_id: normalizeString(order.id),
71
- shipment_id: shipmentId,
72
- status: shipmentStatus,
73
- substatus: shipmentSubstatus,
74
- logistic_type: normalizeString(shipment.logistic_type),
75
- mode: normalizeString(shipment.mode),
76
- currency_id: currencyId,
77
- cost: roundMoney(bucket.total_cost)
78
- })
79
- );
80
- } catch (shipmentError) {
81
- partialFailures.push({
82
- order_id: normalizeString(order.id),
83
- shipment_id: shipmentId,
84
- message: formatMercadoLibreError(shipmentError, `Failed to inspect shipment ${shipmentId}`)
85
- });
412
+ bucket.total_cost += shipmentCost;
413
+ if (shipmentCost > 0) {
414
+ bucket.shipments_with_cost += 1;
415
+ } else {
416
+ bucket.zero_cost_shipments += 1;
417
+ }
418
+ if (dayBucket) {
419
+ const dayMetric = dailyMetrics.get(dayBucket) ?? {
420
+ date: dayBucket,
421
+ shipments: 0,
422
+ delivered_shipments: 0,
423
+ cancelled_shipments: 0,
424
+ not_delivered_shipments: 0,
425
+ shipping_cost_by_currency: {}
426
+ };
427
+ dayMetric.shipments += 1;
428
+ if (shipmentStatus === "delivered") {
429
+ dayMetric.delivered_shipments += 1;
430
+ }
431
+ if (shipmentStatus === "cancelled") {
432
+ dayMetric.cancelled_shipments += 1;
433
+ }
434
+ if (shipmentStatus === "not_delivered") {
435
+ dayMetric.not_delivered_shipments += 1;
436
+ }
437
+ incrementCurrency(dayMetric.shipping_cost_by_currency, currencyId, shipmentCost);
438
+ dailyMetrics.set(dayBucket, dayMetric);
439
+ }
440
+ if (responseMode === "shipments_chunk") {
441
+ shipmentRows.push(
442
+ formatCompactShipmentRow({
443
+ orderId: normalizeScalarString(order?.id),
444
+ shipmentId,
445
+ status: shipmentStatus,
446
+ substatus: shipmentSubstatus,
447
+ logisticType,
448
+ mode,
449
+ currencyId,
450
+ cost: shipmentCost
451
+ })
452
+ );
453
+ }
86
454
  }
87
455
  }
88
456
  for (const metrics of Object.values(costsByCurrency)) {
89
457
  metrics.total_cost = roundMoney(metrics.total_cost);
90
458
  }
459
+ const metadata = buildShippingSummaryMetadata({
460
+ total: paging.total,
461
+ effectiveLimit,
462
+ effectiveOffset,
463
+ returned: allOrders.length,
464
+ responseMode,
465
+ pagesRequested,
466
+ pagesSucceeded,
467
+ pagesFailed: failedPages.length,
468
+ failedOffsets: failedPages.map((failure) => failure.offset),
469
+ shipmentFailures: failures.length
470
+ });
471
+ const calculations = responseMode === "calculations" ? buildShippingCalculations({
472
+ ordersTotal: allOrders.length,
473
+ ordersWithShipping,
474
+ shipmentsRequested: shipmentIds.length,
475
+ shipmentsInspected,
476
+ deliveredShipments,
477
+ cancelledShipments,
478
+ notDeliveredShipments,
479
+ statusCounts,
480
+ substatusCounts,
481
+ logisticTypeCounts,
482
+ modeCounts,
483
+ costsByCurrency,
484
+ dailyMetrics,
485
+ metadata,
486
+ failedPages,
487
+ shipmentFailures: failures
488
+ }) : void 0;
91
489
  return object(
92
490
  stripNulls({
93
491
  profile_id: profileId,
94
- metadata: {
95
- orders_inspected: asArray(ordersResponse.results).length,
96
- shipments_inspected: shipmentRows.length,
97
- partial_failures: partialFailures.length,
98
- has_partial_failures: partialFailures.length > 0
99
- },
100
- status_counts: statusCounts,
101
- substatus_counts: substatusCounts,
102
- costs_by_currency: costsByCurrency,
103
- shipments: shipmentRows,
104
- failures: partialFailures
492
+ metadata,
493
+ shipments_schema: responseMode === "shipments_chunk" ? shipmentsSchema : void 0,
494
+ shipments: responseMode === "shipments_chunk" ? shipmentRows : void 0,
495
+ calculations,
496
+ failures: failures.length > 0 ? failures : void 0,
497
+ failed_pages: failedPages.length > 0 ? failedPages : void 0
105
498
  })
106
499
  );
107
500
  } catch (err) {