@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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/index.js +27 -5
- package/dist/index.js.map +2 -2
- package/dist/mcp-use.json +6 -3
- package/dist/src/services/mercadolibre/mercadolibre-billing.js +77 -0
- package/dist/src/services/mercadolibre/mercadolibre-billing.js.map +7 -0
- package/dist/src/services/mercadolibre/mercadolibre-items.js +29 -2
- package/dist/src/services/mercadolibre/mercadolibre-items.js.map +2 -2
- package/dist/src/services/mercadolibre/mercadolibre-orders.js +18 -0
- package/dist/src/services/mercadolibre/mercadolibre-orders.js.map +2 -2
- package/dist/src/tools/mercadolibre/estimate-listing-fee.js +26 -34
- package/dist/src/tools/mercadolibre/estimate-listing-fee.js.map +2 -2
- package/dist/src/tools/mercadolibre/estimate-product-profitability.js +546 -0
- package/dist/src/tools/mercadolibre/estimate-product-profitability.js.map +7 -0
- package/dist/src/tools/mercadolibre/get-item-details.js +3 -25
- package/dist/src/tools/mercadolibre/get-item-details.js.map +2 -2
- package/dist/src/tools/mercadolibre/get-sales-by-item.js +129 -14
- package/dist/src/tools/mercadolibre/get-sales-by-item.js.map +2 -2
- package/dist/src/tools/mercadolibre/get-sales-trend.js +137 -14
- package/dist/src/tools/mercadolibre/get-sales-trend.js.map +2 -2
- package/dist/src/tools/mercadolibre/get-shipping-summary.js +450 -57
- package/dist/src/tools/mercadolibre/get-shipping-summary.js.map +2 -2
- package/dist/src/tools/mercadolibre/get-site-categories-and-listing-types.js +50 -0
- package/dist/src/tools/mercadolibre/get-site-categories-and-listing-types.js.map +7 -0
- package/dist/src/tools/mercadolibre/index.js +2 -0
- package/dist/src/tools/mercadolibre/index.js.map +2 -2
- package/dist/src/tools/mercadolibre/listing-fee-helpers.js +265 -0
- package/dist/src/tools/mercadolibre/listing-fee-helpers.js.map +7 -0
- package/dist/src/tools/mercadolibre/search-items.js +207 -52
- package/dist/src/tools/mercadolibre/search-items.js.map +2 -2
- 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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
|
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:
|
|
42
|
-
offset:
|
|
258
|
+
limit: requestedLimit,
|
|
259
|
+
offset: requestedOffset
|
|
43
260
|
});
|
|
44
|
-
const
|
|
45
|
-
const
|
|
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
|
|
331
|
+
const dailyMetrics = /* @__PURE__ */ new Map();
|
|
332
|
+
const failures = [];
|
|
48
333
|
const shipmentRows = [];
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
const currencyId = normalizeString(order
|
|
65
|
-
const
|
|
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 +=
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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) {
|