@yoryoboy/bi-mcp 1.16.0 → 1.19.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/README.md +12 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/index.js +179 -6
- package/dist/index.js.map +2 -2
- package/dist/mcp-use.json +3 -6
- package/dist/src/config/__tests__/google-store.test.js +30 -0
- package/dist/src/config/__tests__/google-store.test.js.map +7 -0
- package/dist/src/config/__tests__/mcp-tool-deadline.test.js +64 -0
- package/dist/src/config/__tests__/mcp-tool-deadline.test.js.map +7 -0
- package/dist/src/config/google-store.js +18 -0
- package/dist/src/config/google-store.js.map +2 -2
- package/dist/src/config/mcp-tool-deadline-context.js +34 -0
- package/dist/src/config/mcp-tool-deadline-context.js.map +7 -0
- package/dist/src/config/mcp-tool-deadline.js +67 -0
- package/dist/src/config/mcp-tool-deadline.js.map +7 -0
- package/dist/src/services/mercadolibre/__tests__/mercadolibre-api.test.js +38 -3
- package/dist/src/services/mercadolibre/__tests__/mercadolibre-api.test.js.map +2 -2
- package/dist/src/services/mercadolibre/__tests__/mercadolibre-buyer-reconciliation.test.js +226 -0
- package/dist/src/services/mercadolibre/__tests__/mercadolibre-buyer-reconciliation.test.js.map +7 -0
- package/dist/src/services/mercadolibre/__tests__/mercadolibre-claim-reconciliation.test.js +127 -0
- package/dist/src/services/mercadolibre/__tests__/mercadolibre-claim-reconciliation.test.js.map +7 -0
- package/dist/src/services/mercadolibre/__tests__/mercadolibre-kits.test.js +103 -0
- package/dist/src/services/mercadolibre/__tests__/mercadolibre-kits.test.js.map +7 -0
- package/dist/src/services/mercadolibre/__tests__/mercadolibre-order-billing-info.test.js +582 -0
- package/dist/src/services/mercadolibre/__tests__/mercadolibre-order-billing-info.test.js.map +7 -0
- package/dist/src/services/mercadolibre/__tests__/mercadolibre-orders.test.js +10 -0
- package/dist/src/services/mercadolibre/__tests__/mercadolibre-orders.test.js.map +2 -2
- package/dist/src/services/mercadolibre/__tests__/mercadolibre-packs.test.js +36 -0
- package/dist/src/services/mercadolibre/__tests__/mercadolibre-packs.test.js.map +7 -0
- package/dist/src/services/mercadolibre/__tests__/mercadolibre-payments.test.js +137 -0
- package/dist/src/services/mercadolibre/__tests__/mercadolibre-payments.test.js.map +7 -0
- package/dist/src/services/mercadolibre/__tests__/mercadolibre-product-ads-ad-group-ads.test.js +105 -0
- package/dist/src/services/mercadolibre/__tests__/mercadolibre-product-ads-ad-group-ads.test.js.map +7 -0
- package/dist/src/services/mercadolibre/__tests__/mercadolibre-product-ads-ad-groups.test.js +143 -0
- package/dist/src/services/mercadolibre/__tests__/mercadolibre-product-ads-ad-groups.test.js.map +7 -0
- package/dist/src/services/mercadolibre/__tests__/mercadolibre-product-ads-campaigns.test.js +100 -0
- package/dist/src/services/mercadolibre/__tests__/mercadolibre-product-ads-campaigns.test.js.map +7 -0
- package/dist/src/services/mercadolibre/__tests__/mercadolibre-product-ads.test.js +350 -0
- package/dist/src/services/mercadolibre/__tests__/mercadolibre-product-ads.test.js.map +7 -0
- package/dist/src/services/mercadolibre/__tests__/mercadolibre-return-reconciliation.test.js +228 -0
- package/dist/src/services/mercadolibre/__tests__/mercadolibre-return-reconciliation.test.js.map +7 -0
- package/dist/src/services/mercadolibre/__tests__/mercadolibre-shipping.test.js +58 -0
- package/dist/src/services/mercadolibre/__tests__/mercadolibre-shipping.test.js.map +7 -0
- package/dist/src/services/mercadolibre/mercadolibre-api.js +15 -3
- package/dist/src/services/mercadolibre/mercadolibre-api.js.map +2 -2
- package/dist/src/services/mercadolibre/mercadolibre-billing.js +110 -3
- package/dist/src/services/mercadolibre/mercadolibre-billing.js.map +2 -2
- package/dist/src/services/mercadolibre/mercadolibre-buyer-reconciliation.js +368 -0
- package/dist/src/services/mercadolibre/mercadolibre-buyer-reconciliation.js.map +7 -0
- package/dist/src/services/mercadolibre/mercadolibre-claim-reconciliation.js +399 -0
- package/dist/src/services/mercadolibre/mercadolibre-claim-reconciliation.js.map +7 -0
- package/dist/src/services/mercadolibre/mercadolibre-kits.js +55 -0
- package/dist/src/services/mercadolibre/mercadolibre-kits.js.map +7 -0
- package/dist/src/services/mercadolibre/mercadolibre-order-billing-info.js +392 -0
- package/dist/src/services/mercadolibre/mercadolibre-order-billing-info.js.map +7 -0
- package/dist/src/services/mercadolibre/mercadolibre-orders.js +8 -8
- package/dist/src/services/mercadolibre/mercadolibre-orders.js.map +2 -2
- package/dist/src/services/mercadolibre/mercadolibre-packs.js +21 -0
- package/dist/src/services/mercadolibre/mercadolibre-packs.js.map +7 -0
- package/dist/src/services/mercadolibre/mercadolibre-payments.js +60 -0
- package/dist/src/services/mercadolibre/mercadolibre-payments.js.map +7 -0
- package/dist/src/services/mercadolibre/mercadolibre-product-ads.js +429 -0
- package/dist/src/services/mercadolibre/mercadolibre-product-ads.js.map +7 -0
- package/dist/src/services/mercadolibre/mercadolibre-return-reconciliation.js +436 -0
- package/dist/src/services/mercadolibre/mercadolibre-return-reconciliation.js.map +7 -0
- package/dist/src/services/mercadolibre/mercadolibre-shipping.js +35 -0
- package/dist/src/services/mercadolibre/mercadolibre-shipping.js.map +7 -0
- package/dist/src/services/vtex/__tests__/vtex-api.test.js +67 -0
- package/dist/src/services/vtex/__tests__/vtex-api.test.js.map +7 -0
- package/dist/src/services/vtex/vtex-api.js +6 -0
- package/dist/src/services/vtex/vtex-api.js.map +2 -2
- package/dist/src/tools/config/__tests__/list-profiles.test.js +154 -0
- package/dist/src/tools/config/__tests__/list-profiles.test.js.map +7 -0
- package/dist/src/tools/config/list-profiles.js +39 -3
- package/dist/src/tools/config/list-profiles.js.map +2 -2
- package/dist/src/tools/mercadolibre/__tests__/get-order-billing-info.test.js +339 -0
- package/dist/src/tools/mercadolibre/__tests__/get-order-billing-info.test.js.map +7 -0
- package/dist/src/tools/mercadolibre/__tests__/product-ads-ad-group-ads.test.js +225 -0
- package/dist/src/tools/mercadolibre/__tests__/product-ads-ad-group-ads.test.js.map +7 -0
- package/dist/src/tools/mercadolibre/__tests__/product-ads-ad-groups.test.js +148 -0
- package/dist/src/tools/mercadolibre/__tests__/product-ads-ad-groups.test.js.map +7 -0
- package/dist/src/tools/mercadolibre/__tests__/product-ads-campaigns.test.js +60 -0
- package/dist/src/tools/mercadolibre/__tests__/product-ads-campaigns.test.js.map +7 -0
- package/dist/src/tools/mercadolibre/__tests__/product-ads-helpers.test.js +128 -0
- package/dist/src/tools/mercadolibre/__tests__/product-ads-helpers.test.js.map +7 -0
- package/dist/src/tools/mercadolibre/__tests__/product-ads-tools.test.js +435 -0
- package/dist/src/tools/mercadolibre/__tests__/product-ads-tools.test.js.map +7 -0
- package/dist/src/tools/mercadolibre/__tests__/reconcile-buyer-data.test.js +31 -0
- package/dist/src/tools/mercadolibre/__tests__/reconcile-buyer-data.test.js.map +7 -0
- package/dist/src/tools/mercadolibre/__tests__/reconcile-claim-data.test.js +36 -0
- package/dist/src/tools/mercadolibre/__tests__/reconcile-claim-data.test.js.map +7 -0
- package/dist/src/tools/mercadolibre/__tests__/validate-connection.test.js +205 -0
- package/dist/src/tools/mercadolibre/__tests__/validate-connection.test.js.map +7 -0
- package/dist/src/tools/mercadolibre/get-billing-provisions-by-order.js +313 -0
- package/dist/src/tools/mercadolibre/get-billing-provisions-by-order.js.map +7 -0
- package/dist/src/tools/mercadolibre/get-bundle-context-by-item.js +176 -0
- package/dist/src/tools/mercadolibre/get-bundle-context-by-item.js.map +7 -0
- package/dist/src/tools/mercadolibre/get-full-storage-charges.js +183 -0
- package/dist/src/tools/mercadolibre/get-full-storage-charges.js.map +7 -0
- package/dist/src/tools/mercadolibre/get-order-billing-info.js +189 -0
- package/dist/src/tools/mercadolibre/get-order-billing-info.js.map +7 -0
- package/dist/src/tools/mercadolibre/get-pack-context-by-order.js +150 -0
- package/dist/src/tools/mercadolibre/get-pack-context-by-order.js.map +7 -0
- package/dist/src/tools/mercadolibre/get-payment-details.js +322 -0
- package/dist/src/tools/mercadolibre/get-payment-details.js.map +7 -0
- package/dist/src/tools/mercadolibre/get-product-ads-ad-group-ads.js +144 -0
- package/dist/src/tools/mercadolibre/get-product-ads-ad-group-ads.js.map +7 -0
- package/dist/src/tools/mercadolibre/get-product-ads-advertisers.js +59 -0
- package/dist/src/tools/mercadolibre/get-product-ads-advertisers.js.map +7 -0
- package/dist/src/tools/mercadolibre/get-shipping-quote-by-declared-dimensions.js +104 -0
- package/dist/src/tools/mercadolibre/get-shipping-quote-by-declared-dimensions.js.map +7 -0
- package/dist/src/tools/mercadolibre/index.js +32 -0
- package/dist/src/tools/mercadolibre/index.js.map +2 -2
- package/dist/src/tools/mercadolibre/product-ads-helpers.js +130 -0
- package/dist/src/tools/mercadolibre/product-ads-helpers.js.map +7 -0
- package/dist/src/tools/mercadolibre/reconcile-buyer-data.js +27 -0
- package/dist/src/tools/mercadolibre/reconcile-buyer-data.js.map +7 -0
- package/dist/src/tools/mercadolibre/reconcile-claim-data.js +46 -0
- package/dist/src/tools/mercadolibre/reconcile-claim-data.js.map +7 -0
- package/dist/src/tools/mercadolibre/reconcile-return-data.js +25 -0
- package/dist/src/tools/mercadolibre/reconcile-return-data.js.map +7 -0
- package/dist/src/tools/mercadolibre/search-product-ads-ad-groups.js +162 -0
- package/dist/src/tools/mercadolibre/search-product-ads-ad-groups.js.map +7 -0
- package/dist/src/tools/mercadolibre/search-product-ads-ads.js +216 -0
- package/dist/src/tools/mercadolibre/search-product-ads-ads.js.map +7 -0
- package/dist/src/tools/mercadolibre/search-product-ads-campaigns.js +116 -0
- package/dist/src/tools/mercadolibre/search-product-ads-campaigns.js.map +7 -0
- package/dist/src/tools/mercadolibre/validate-connection.js +40 -12
- package/dist/src/tools/mercadolibre/validate-connection.js.map +2 -2
- package/dist/tests/meli/mercadolibre-billing-provisions.test.js +378 -0
- package/dist/tests/meli/mercadolibre-billing-provisions.test.js.map +2 -2
- package/dist/tests/meli/mercadolibre-order-billing-info-surface.test.js +123 -0
- package/dist/tests/meli/mercadolibre-order-billing-info-surface.test.js.map +7 -0
- package/dist/tests/meli/mercadolibre-payment-details.test.js +356 -0
- package/dist/tests/meli/mercadolibre-payment-details.test.js.map +7 -0
- package/dist/tests/meli/mercadolibre-payments.test.js +62 -0
- package/dist/tests/meli/mercadolibre-payments.test.js.map +7 -0
- package/dist/tests/meli/mercadolibre-product-ads-ad-groups-surface.test.js +90 -0
- package/dist/tests/meli/mercadolibre-product-ads-ad-groups-surface.test.js.map +7 -0
- package/dist/tests/meli/mercadolibre-product-ads-campaigns-surface.test.js +72 -0
- package/dist/tests/meli/mercadolibre-product-ads-campaigns-surface.test.js.map +7 -0
- package/dist/tests/meli/mercadolibre-tool-handlers-pack-kit-shipping.test.js +400 -0
- package/dist/tests/meli/mercadolibre-tool-handlers-pack-kit-shipping.test.js.map +7 -0
- package/dist/tests/meli/mercadolibre-tool-handlers-small.test.js +31 -12
- package/dist/tests/meli/mercadolibre-tool-handlers-small.test.js.map +2 -2
- package/dist/tests/vtex/vtex-commercial-conditions-docs.test.js +1 -1
- package/dist/tests/vtex/vtex-commercial-conditions-docs.test.js.map +1 -1
- package/package.json +1 -1
- package/dist/src/google-ads/report-utils.js +0 -78
- package/dist/src/google-ads/report-utils.js.map +0 -7
- package/dist/src/search-console/search-console-utils.js +0 -275
- package/dist/src/search-console/search-console-utils.js.map +0 -7
- package/dist/src/services/vtex/__tests__/vtex-catalog-write-batch.test.js +0 -165
- package/dist/src/services/vtex/__tests__/vtex-catalog-write-batch.test.js.map +0 -7
- package/dist/src/tools/vtex/__tests__/catalog-admin-batch.test.js +0 -233
- package/dist/src/tools/vtex/__tests__/catalog-admin-batch.test.js.map +0 -7
- package/dist/src/tools/vtex/catalog-admin-batch.js +0 -298
- package/dist/src/tools/vtex/catalog-admin-batch.js.map +0 -7
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { getMercadoLibreOrderBillingInfo, getMercadoLibreSaleBillingInfo } from "../mercadolibre-order-billing-info.js";
|
|
3
|
+
import { createMercadoLibreClient, MercadoLibreApiError } from "../mercadolibre-api.js";
|
|
4
|
+
import { getMercadoLibreConnectionRow } from "../../../config/mercadolibre-profile-store.js";
|
|
5
|
+
import { assertActiveProfile } from "../../../config/profile-store.js";
|
|
6
|
+
import { decryptSecret } from "../../../config/vtex-crypto.js";
|
|
7
|
+
vi.mock("../mercadolibre-api.js", () => ({
|
|
8
|
+
createMercadoLibreClient: vi.fn(),
|
|
9
|
+
isRetryableMercadoLibreError: (error) => error instanceof MercadoLibreApiError && Boolean(error.status && [408, 429, 500, 502, 503, 504].includes(error.status)),
|
|
10
|
+
MercadoLibreApiError: class MercadoLibreApiError extends Error {
|
|
11
|
+
constructor(message, status, code, retryAfterMs) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.status = status;
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.retryAfterMs = retryAfterMs;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}));
|
|
19
|
+
vi.mock("../../../config/mercadolibre-profile-store.js", () => ({ getMercadoLibreConnectionRow: vi.fn() }));
|
|
20
|
+
vi.mock("../../../config/profile-store.js", () => ({ assertActiveProfile: vi.fn() }));
|
|
21
|
+
vi.mock("../../../config/vtex-crypto.js", () => ({ decryptSecret: vi.fn() }));
|
|
22
|
+
describe("getMercadoLibreOrderBillingInfo", () => {
|
|
23
|
+
const getMock = vi.fn();
|
|
24
|
+
const completeBillingInfo = () => ({
|
|
25
|
+
buyer: {
|
|
26
|
+
billing_info: {
|
|
27
|
+
name: "Ada",
|
|
28
|
+
last_name: "Lovelace",
|
|
29
|
+
identification: { type: "CUIT", number: "20123456789" },
|
|
30
|
+
address: {
|
|
31
|
+
street_name: "Main",
|
|
32
|
+
street_number: "10",
|
|
33
|
+
city_name: "Buenos Aires",
|
|
34
|
+
state: "CABA",
|
|
35
|
+
zip_code: "1000",
|
|
36
|
+
country_id: "AR"
|
|
37
|
+
},
|
|
38
|
+
taxes: { taxpayer_type: { id: "responsable_inscripto", description: "Responsable Inscripto" } }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
function createDeferred() {
|
|
43
|
+
let resolve;
|
|
44
|
+
const promise = new Promise((resolvePromise) => {
|
|
45
|
+
resolve = resolvePromise;
|
|
46
|
+
});
|
|
47
|
+
return { promise, resolve };
|
|
48
|
+
}
|
|
49
|
+
function setFiscalField(billingInfo, path, value) {
|
|
50
|
+
let current = billingInfo.buyer;
|
|
51
|
+
for (const key of ["billing_info", ...path.slice(0, -1)]) {
|
|
52
|
+
current = current[key];
|
|
53
|
+
}
|
|
54
|
+
current[path.at(-1)] = value;
|
|
55
|
+
}
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
vi.clearAllMocks();
|
|
58
|
+
vi.mocked(assertActiveProfile).mockResolvedValue({ id: "triplim" });
|
|
59
|
+
vi.mocked(getMercadoLibreConnectionRow).mockResolvedValue({
|
|
60
|
+
sellerId: "seller-1",
|
|
61
|
+
encryptedAccessToken: "stored-token",
|
|
62
|
+
expiresAt: /* @__PURE__ */ new Date("2099-01-01T00:00:00.000Z"),
|
|
63
|
+
status: "active"
|
|
64
|
+
});
|
|
65
|
+
vi.mocked(decryptSecret).mockReturnValue("stored-access-token");
|
|
66
|
+
vi.mocked(createMercadoLibreClient).mockReturnValue({ get: getMock });
|
|
67
|
+
});
|
|
68
|
+
it("reads the order before requesting billing info with its site and billing info IDs", async () => {
|
|
69
|
+
const order = { seller: { id: "seller-1" }, buyer: { billing_info: { id: "billing/123" } } };
|
|
70
|
+
const billingInfo = completeBillingInfo();
|
|
71
|
+
getMock.mockResolvedValueOnce({ data: order }).mockResolvedValueOnce({ data: { site_id: "MLA" } }).mockResolvedValueOnce({ data: billingInfo });
|
|
72
|
+
await expect(getMercadoLibreOrderBillingInfo("triplim", "order/456")).resolves.toEqual({
|
|
73
|
+
available: true,
|
|
74
|
+
order,
|
|
75
|
+
billingInfo
|
|
76
|
+
});
|
|
77
|
+
expect(getMock).toHaveBeenNthCalledWith(1, "/orders/order%2F456");
|
|
78
|
+
expect(getMock).toHaveBeenNthCalledWith(2, "/users/seller-1");
|
|
79
|
+
expect(getMock).toHaveBeenNthCalledWith(3, "/orders/billing-info/MLA/billing%2F123");
|
|
80
|
+
});
|
|
81
|
+
it("returns an explicit unavailable result when the order has no buyer billing info", async () => {
|
|
82
|
+
getMock.mockResolvedValueOnce({ data: { seller: { id: "seller-1" }, buyer: {} } });
|
|
83
|
+
await expect(getMercadoLibreOrderBillingInfo("triplim", "456")).resolves.toEqual({
|
|
84
|
+
available: false,
|
|
85
|
+
reason: "order_buyer_billing_info_missing"
|
|
86
|
+
});
|
|
87
|
+
expect(getMock).toHaveBeenCalledTimes(1);
|
|
88
|
+
});
|
|
89
|
+
it("does not request fiscal data when the order belongs to another seller", async () => {
|
|
90
|
+
getMock.mockResolvedValueOnce({
|
|
91
|
+
data: { seller: { id: "other-seller" }, buyer: { billing_info: { id: "billing/123" } } }
|
|
92
|
+
});
|
|
93
|
+
await expect(getMercadoLibreOrderBillingInfo("triplim", "456")).resolves.toEqual({
|
|
94
|
+
available: false,
|
|
95
|
+
reason: "order_seller_ownership_mismatch"
|
|
96
|
+
});
|
|
97
|
+
expect(getMock).toHaveBeenCalledTimes(1);
|
|
98
|
+
expect(getMock).not.toHaveBeenCalledWith(expect.stringContaining("/orders/billing-info/"));
|
|
99
|
+
});
|
|
100
|
+
it("accepts a numeric order seller ID for the selected string seller ID", async () => {
|
|
101
|
+
vi.mocked(getMercadoLibreConnectionRow).mockResolvedValue({
|
|
102
|
+
sellerId: "123",
|
|
103
|
+
encryptedAccessToken: "stored-token",
|
|
104
|
+
expiresAt: /* @__PURE__ */ new Date("2099-01-01T00:00:00.000Z"),
|
|
105
|
+
status: "active"
|
|
106
|
+
});
|
|
107
|
+
const order = { seller: { id: 123 }, buyer: { billing_info: { id: "billing/123" } } };
|
|
108
|
+
const billingInfo = completeBillingInfo();
|
|
109
|
+
getMock.mockResolvedValueOnce({ data: order }).mockResolvedValueOnce({ data: { site_id: "MLA" } }).mockResolvedValueOnce({ data: billingInfo });
|
|
110
|
+
await expect(getMercadoLibreOrderBillingInfo("triplim", "456")).resolves.toEqual({
|
|
111
|
+
available: true,
|
|
112
|
+
order,
|
|
113
|
+
billingInfo
|
|
114
|
+
});
|
|
115
|
+
expect(getMock).toHaveBeenNthCalledWith(2, "/users/123");
|
|
116
|
+
expect(getMock).toHaveBeenNthCalledWith(3, "/orders/billing-info/MLA/billing%2F123");
|
|
117
|
+
});
|
|
118
|
+
it("normalizes surrounding whitespace before checking seller ownership", async () => {
|
|
119
|
+
vi.mocked(getMercadoLibreConnectionRow).mockResolvedValue({
|
|
120
|
+
sellerId: " seller-1 ",
|
|
121
|
+
encryptedAccessToken: "stored-token",
|
|
122
|
+
expiresAt: /* @__PURE__ */ new Date("2099-01-01T00:00:00.000Z"),
|
|
123
|
+
status: "active"
|
|
124
|
+
});
|
|
125
|
+
const order = { seller: { id: " seller-1 " }, buyer: { billing_info: { id: "billing/123" } } };
|
|
126
|
+
getMock.mockResolvedValueOnce({ data: order }).mockResolvedValueOnce({ data: { site_id: "MLA" } }).mockResolvedValueOnce({ data: completeBillingInfo() });
|
|
127
|
+
await expect(getMercadoLibreOrderBillingInfo("triplim", "456")).resolves.toMatchObject({ available: true });
|
|
128
|
+
expect(getMock).toHaveBeenNthCalledWith(2, "/users/%20seller-1%20");
|
|
129
|
+
expect(getMock).toHaveBeenNthCalledWith(3, "/orders/billing-info/MLA/billing%2F123");
|
|
130
|
+
});
|
|
131
|
+
it("does not request fiscal data when the order seller data is malformed", async () => {
|
|
132
|
+
getMock.mockResolvedValueOnce({
|
|
133
|
+
data: { seller: { id: { unexpected: "value" } }, buyer: { billing_info: { id: "billing/123" } } }
|
|
134
|
+
});
|
|
135
|
+
await expect(getMercadoLibreOrderBillingInfo("triplim", "456")).resolves.toEqual({
|
|
136
|
+
available: false,
|
|
137
|
+
reason: "order_seller_id_missing_or_malformed"
|
|
138
|
+
});
|
|
139
|
+
expect(getMock).toHaveBeenCalledTimes(1);
|
|
140
|
+
expect(getMock).not.toHaveBeenCalledWith(expect.stringContaining("/orders/billing-info/"));
|
|
141
|
+
});
|
|
142
|
+
it.each([
|
|
143
|
+
["null", null],
|
|
144
|
+
["array", []],
|
|
145
|
+
["empty string", ""],
|
|
146
|
+
["whitespace-only string", " "]
|
|
147
|
+
])("fails closed for a %s seller ID", async (_description, sellerId) => {
|
|
148
|
+
getMock.mockResolvedValueOnce({
|
|
149
|
+
data: { seller: { id: sellerId }, buyer: { billing_info: { id: "billing/123" } } }
|
|
150
|
+
});
|
|
151
|
+
const result = await getMercadoLibreOrderBillingInfo("triplim", "456");
|
|
152
|
+
expect(result).toEqual({
|
|
153
|
+
available: false,
|
|
154
|
+
reason: "order_seller_id_missing_or_malformed"
|
|
155
|
+
});
|
|
156
|
+
expect(result).not.toHaveProperty("billingInfo");
|
|
157
|
+
expect(result).not.toHaveProperty("fiscal");
|
|
158
|
+
expect(getMock).toHaveBeenCalledTimes(1);
|
|
159
|
+
expect(getMock).not.toHaveBeenCalledWith(expect.stringContaining("/orders/billing-info/"));
|
|
160
|
+
});
|
|
161
|
+
it("preserves upstream errors from the order or billing-info request", async () => {
|
|
162
|
+
const upstreamError = new Error("MercadoLibre API request failed (403): forbidden");
|
|
163
|
+
getMock.mockResolvedValueOnce({ data: { seller: { id: "seller-1" }, buyer: { billing_info: { id: "123" } } } }).mockResolvedValueOnce({ data: { site_id: "MLA" } }).mockRejectedValueOnce(upstreamError);
|
|
164
|
+
await expect(getMercadoLibreOrderBillingInfo("triplim", "456")).rejects.toMatchObject({
|
|
165
|
+
message: "MercadoLibre billing info request unavailable"
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
it("returns credential maintenance required without refreshing or persisting credentials", async () => {
|
|
169
|
+
vi.mocked(getMercadoLibreConnectionRow).mockResolvedValue({
|
|
170
|
+
sellerId: "seller-1",
|
|
171
|
+
encryptedAccessToken: "stored-token",
|
|
172
|
+
expiresAt: /* @__PURE__ */ new Date("2000-01-01T00:00:00.000Z"),
|
|
173
|
+
status: "active"
|
|
174
|
+
});
|
|
175
|
+
await expect(getMercadoLibreOrderBillingInfo("triplim", "456")).resolves.toEqual({
|
|
176
|
+
available: false,
|
|
177
|
+
reason: "credential_maintenance_required"
|
|
178
|
+
});
|
|
179
|
+
expect(decryptSecret).not.toHaveBeenCalled();
|
|
180
|
+
expect(createMercadoLibreClient).not.toHaveBeenCalled();
|
|
181
|
+
expect(getMock).not.toHaveBeenCalled();
|
|
182
|
+
});
|
|
183
|
+
it("retries a transient order request with deterministic backoff", async () => {
|
|
184
|
+
const transient = new MercadoLibreApiError("temporary", 503);
|
|
185
|
+
const pause = vi.fn().mockResolvedValue(void 0);
|
|
186
|
+
getMock.mockRejectedValueOnce(transient).mockResolvedValueOnce({ data: { seller: { id: "seller-1" }, buyer: { billing_info: { id: "123" } } } }).mockResolvedValueOnce({ data: { site_id: "MLA" } }).mockResolvedValueOnce({ data: completeBillingInfo() });
|
|
187
|
+
await expect(
|
|
188
|
+
getMercadoLibreOrderBillingInfo("triplim", "456", { baseRetryDelayMs: 1, sleep: pause })
|
|
189
|
+
).resolves.toMatchObject({ available: true });
|
|
190
|
+
expect(pause).toHaveBeenCalledWith(1);
|
|
191
|
+
expect(getMock).toHaveBeenCalledTimes(4);
|
|
192
|
+
});
|
|
193
|
+
it("retries transient account and billing-info requests", async () => {
|
|
194
|
+
const pause = vi.fn().mockResolvedValue(void 0);
|
|
195
|
+
getMock.mockResolvedValueOnce({ data: { seller: { id: "seller-1" }, buyer: { billing_info: { id: "123" } } } }).mockRejectedValueOnce(new MercadoLibreApiError("temporary account", 503)).mockResolvedValueOnce({ data: { site_id: "MLA" } }).mockRejectedValueOnce(new MercadoLibreApiError("temporary billing", 429)).mockResolvedValueOnce({ data: completeBillingInfo() });
|
|
196
|
+
await expect(
|
|
197
|
+
getMercadoLibreOrderBillingInfo("triplim", "456", { baseRetryDelayMs: 1, sleep: pause })
|
|
198
|
+
).resolves.toMatchObject({ available: true });
|
|
199
|
+
expect(pause).toHaveBeenCalledTimes(2);
|
|
200
|
+
expect(getMock).toHaveBeenCalledTimes(5);
|
|
201
|
+
});
|
|
202
|
+
it("preserves terminal retryable classification after bounded retry exhaustion without leaking upstream details", async () => {
|
|
203
|
+
const pause = vi.fn().mockResolvedValue(void 0);
|
|
204
|
+
getMock.mockRejectedValue(new MercadoLibreApiError("buyer-tax-id-should-not-leak", 503, "internal_error"));
|
|
205
|
+
await expect(
|
|
206
|
+
getMercadoLibreOrderBillingInfo("triplim", "456", { maxRetries: 1, sleep: pause })
|
|
207
|
+
).rejects.toMatchObject({
|
|
208
|
+
message: "MercadoLibre billing info request unavailable",
|
|
209
|
+
status: 503,
|
|
210
|
+
code: "internal_error",
|
|
211
|
+
resource: "order",
|
|
212
|
+
attempts: 2,
|
|
213
|
+
retryable: true
|
|
214
|
+
});
|
|
215
|
+
expect(pause).toHaveBeenCalledTimes(1);
|
|
216
|
+
expect(getMock).toHaveBeenCalledTimes(2);
|
|
217
|
+
});
|
|
218
|
+
it.each([
|
|
219
|
+
[404, "not_found", "order"],
|
|
220
|
+
[403, "forbidden", "order"]
|
|
221
|
+
])("preserves terminal %i non-retryable classification without retrying", async (status, code, resource) => {
|
|
222
|
+
const pause = vi.fn().mockResolvedValue(void 0);
|
|
223
|
+
getMock.mockRejectedValue(new MercadoLibreApiError("buyer-tax-id-should-not-leak", status, code));
|
|
224
|
+
await expect(
|
|
225
|
+
getMercadoLibreOrderBillingInfo("triplim", "456", { sleep: pause })
|
|
226
|
+
).rejects.toMatchObject({
|
|
227
|
+
message: "MercadoLibre billing info request unavailable",
|
|
228
|
+
status,
|
|
229
|
+
code,
|
|
230
|
+
resource,
|
|
231
|
+
attempts: 1,
|
|
232
|
+
retryable: false
|
|
233
|
+
});
|
|
234
|
+
expect(pause).not.toHaveBeenCalled();
|
|
235
|
+
expect(getMock).toHaveBeenCalledTimes(1);
|
|
236
|
+
});
|
|
237
|
+
it("fails closed when intermediary or billing payloads are malformed", async () => {
|
|
238
|
+
getMock.mockResolvedValueOnce({ data: { seller: { id: "seller-1" }, buyer: { billing_info: { id: "123" } } } }).mockResolvedValueOnce({ data: [] });
|
|
239
|
+
await expect(getMercadoLibreOrderBillingInfo("triplim", "456")).resolves.toEqual({
|
|
240
|
+
available: false,
|
|
241
|
+
reason: "account_payload_malformed"
|
|
242
|
+
});
|
|
243
|
+
getMock.mockReset();
|
|
244
|
+
getMock.mockResolvedValueOnce({ data: { seller: { id: "seller-1" }, buyer: { billing_info: { id: "123" } } } }).mockResolvedValueOnce({ data: { site_id: "MLA" } }).mockResolvedValueOnce({ data: { buyer: {} } });
|
|
245
|
+
await expect(getMercadoLibreOrderBillingInfo("triplim", "456")).resolves.toEqual({
|
|
246
|
+
available: false,
|
|
247
|
+
reason: "billing_info_payload_malformed"
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
it.each([
|
|
251
|
+
["buyer name", { name: true }],
|
|
252
|
+
["buyer last name", { last_name: true }],
|
|
253
|
+
["identification type", { identification: { type: true } }],
|
|
254
|
+
["identification number", { identification: { number: true } }],
|
|
255
|
+
["address street name", { address: { street_name: true } }],
|
|
256
|
+
["address street number", { address: { street_number: true } }],
|
|
257
|
+
["address city", { address: { city_name: true } }],
|
|
258
|
+
["address state", { address: { state: true } }],
|
|
259
|
+
["address zip code", { address: { zip_code: true } }],
|
|
260
|
+
["address country", { address: { country_id: true } }],
|
|
261
|
+
["taxpayer type ID", { taxes: { taxpayer_type: { id: true } } }],
|
|
262
|
+
["taxpayer type description", { taxes: { taxpayer_type: { description: true } } }]
|
|
263
|
+
])("fails closed when %s is a boolean", async (_field, fiscalFields) => {
|
|
264
|
+
getMock.mockResolvedValueOnce({ data: { seller: { id: "seller-1" }, buyer: { billing_info: { id: "123" } } } }).mockResolvedValueOnce({ data: { site_id: "MLA" } }).mockResolvedValueOnce({ data: { buyer: { billing_info: fiscalFields } } });
|
|
265
|
+
const result = await getMercadoLibreOrderBillingInfo("triplim", "456");
|
|
266
|
+
expect(result).toEqual({
|
|
267
|
+
available: false,
|
|
268
|
+
reason: "billing_info_fiscal_fields_malformed"
|
|
269
|
+
});
|
|
270
|
+
expect(result).not.toHaveProperty("billingInfo");
|
|
271
|
+
});
|
|
272
|
+
it.each([
|
|
273
|
+
["buyer name", ["name"]],
|
|
274
|
+
["buyer last name", ["last_name"]],
|
|
275
|
+
["identification type", ["identification", "type"]],
|
|
276
|
+
["identification number", ["identification", "number"]],
|
|
277
|
+
["address street name", ["address", "street_name"]],
|
|
278
|
+
["address street number", ["address", "street_number"]],
|
|
279
|
+
["address city", ["address", "city_name"]],
|
|
280
|
+
["address state", ["address", "state"]],
|
|
281
|
+
["address zip code", ["address", "zip_code"]],
|
|
282
|
+
["address country", ["address", "country_id"]],
|
|
283
|
+
["taxpayer type ID", ["taxes", "taxpayer_type", "id"]],
|
|
284
|
+
["taxpayer type description", ["taxes", "taxpayer_type", "description"]]
|
|
285
|
+
])("fails closed without a fiscal projection when required %s is null or blank", async (_field, path) => {
|
|
286
|
+
for (const value of [null, " "]) {
|
|
287
|
+
const billingInfo = completeBillingInfo();
|
|
288
|
+
setFiscalField(billingInfo, path, value);
|
|
289
|
+
getMock.mockResolvedValueOnce({ data: { seller: { id: "seller-1" }, buyer: { billing_info: { id: "123" } } } }).mockResolvedValueOnce({ data: { site_id: "MLA" } }).mockResolvedValueOnce({ data: billingInfo });
|
|
290
|
+
const result = await getMercadoLibreOrderBillingInfo("triplim", "456");
|
|
291
|
+
expect(result).toEqual({ available: false, reason: "billing_info_fiscal_fields_missing" });
|
|
292
|
+
expect(result).not.toHaveProperty("billingInfo");
|
|
293
|
+
expect(result).not.toHaveProperty("fiscal");
|
|
294
|
+
getMock.mockReset();
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
it.each([
|
|
298
|
+
["array", []],
|
|
299
|
+
["object", { unexpected: "value" }]
|
|
300
|
+
])("fails closed when a fiscal field is a malformed %s", async (_type, value) => {
|
|
301
|
+
getMock.mockResolvedValueOnce({ data: { seller: { id: "seller-1" }, buyer: { billing_info: { id: "123" } } } }).mockResolvedValueOnce({ data: { site_id: "MLA" } }).mockResolvedValueOnce({ data: { buyer: { billing_info: { name: value } } } });
|
|
302
|
+
await expect(getMercadoLibreOrderBillingInfo("triplim", "456")).resolves.toEqual({
|
|
303
|
+
available: false,
|
|
304
|
+
reason: "billing_info_fiscal_fields_malformed"
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
it("allows a valid numeric street number in a complete fiscal record", async () => {
|
|
308
|
+
const billingInfo = completeBillingInfo();
|
|
309
|
+
setFiscalField(billingInfo, ["address", "street_number"], 123);
|
|
310
|
+
getMock.mockResolvedValueOnce({ data: { seller: { id: "seller-1" }, buyer: { billing_info: { id: "123" } } } }).mockResolvedValueOnce({ data: { site_id: "MLA" } }).mockResolvedValueOnce({ data: billingInfo });
|
|
311
|
+
await expect(getMercadoLibreOrderBillingInfo("triplim", "456")).resolves.toEqual({
|
|
312
|
+
available: true,
|
|
313
|
+
order: { seller: { id: "seller-1" }, buyer: { billing_info: { id: "123" } } },
|
|
314
|
+
billingInfo
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
it("normalizes a complete Mercado Libre state object to the report display state", async () => {
|
|
318
|
+
const billingInfo = completeBillingInfo();
|
|
319
|
+
setFiscalField(billingInfo, ["address", "state"], { code: "AR-C", name: "Ciudad Aut\xF3noma de Buenos Aires" });
|
|
320
|
+
getMock.mockResolvedValueOnce({ data: { seller: { id: "seller-1" }, buyer: { billing_info: { id: "123" } } } }).mockResolvedValueOnce({ data: { site_id: "MLA" } }).mockResolvedValueOnce({ data: billingInfo });
|
|
321
|
+
await expect(getMercadoLibreOrderBillingInfo("triplim", "456")).resolves.toEqual({
|
|
322
|
+
available: true,
|
|
323
|
+
order: { seller: { id: "seller-1" }, buyer: { billing_info: { id: "123" } } },
|
|
324
|
+
billingInfo: expect.objectContaining({
|
|
325
|
+
buyer: expect.objectContaining({
|
|
326
|
+
billing_info: expect.objectContaining({
|
|
327
|
+
address: expect.objectContaining({ state: "Ciudad Aut\xF3noma de Buenos Aires" })
|
|
328
|
+
})
|
|
329
|
+
})
|
|
330
|
+
})
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
it.each([
|
|
334
|
+
["missing code", { name: "Ciudad Aut\xF3noma de Buenos Aires" }],
|
|
335
|
+
["missing name", { code: "AR-C" }],
|
|
336
|
+
["blank name", { code: "AR-C", name: " " }]
|
|
337
|
+
])("fails closed for a state object with %s", async (_description, state) => {
|
|
338
|
+
const billingInfo = completeBillingInfo();
|
|
339
|
+
setFiscalField(billingInfo, ["address", "state"], state);
|
|
340
|
+
getMock.mockResolvedValueOnce({ data: { seller: { id: "seller-1" }, buyer: { billing_info: { id: "123" } } } }).mockResolvedValueOnce({ data: { site_id: "MLA" } }).mockResolvedValueOnce({ data: billingInfo });
|
|
341
|
+
await expect(getMercadoLibreOrderBillingInfo("triplim", "456")).resolves.toEqual({
|
|
342
|
+
available: false,
|
|
343
|
+
reason: "billing_info_fiscal_fields_malformed"
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
it("retains scalar state compatibility", async () => {
|
|
347
|
+
const billingInfo = completeBillingInfo();
|
|
348
|
+
getMock.mockResolvedValueOnce({ data: { seller: { id: "seller-1" }, buyer: { billing_info: { id: "123" } } } }).mockResolvedValueOnce({ data: { site_id: "MLA" } }).mockResolvedValueOnce({ data: billingInfo });
|
|
349
|
+
await expect(getMercadoLibreOrderBillingInfo("triplim", "456")).resolves.toEqual({
|
|
350
|
+
available: true,
|
|
351
|
+
order: { seller: { id: "seller-1" }, buyer: { billing_info: { id: "123" } } },
|
|
352
|
+
billingInfo
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
it("resolves a direct sale reference as an order without querying packs", async () => {
|
|
356
|
+
getMock.mockResolvedValueOnce({ data: { seller: { id: "seller-1" }, buyer: { billing_info: { id: "123" } } } }).mockResolvedValueOnce({ data: { site_id: "MLA" } }).mockResolvedValueOnce({ data: completeBillingInfo() });
|
|
357
|
+
await expect(getMercadoLibreSaleBillingInfo("triplim", "order-1")).resolves.toMatchObject({
|
|
358
|
+
resolutionKind: "order",
|
|
359
|
+
orderId: "order-1",
|
|
360
|
+
result: { available: true }
|
|
361
|
+
});
|
|
362
|
+
expect(getMock).not.toHaveBeenCalledWith("/packs/order-1");
|
|
363
|
+
});
|
|
364
|
+
it("falls back to a pack only after an order 404 and independently resolves every member", async () => {
|
|
365
|
+
const completeFor = (id) => ({ seller: { id: "seller-1" }, buyer: { billing_info: { id } } });
|
|
366
|
+
getMock.mockImplementation((path) => {
|
|
367
|
+
if (path === "/orders/pack-1") return Promise.reject(new MercadoLibreApiError("not found", 404));
|
|
368
|
+
if (path === "/packs/pack-1") return Promise.resolve({ data: { orders: [{ id: "order-1" }, { id: "order-2" }] } });
|
|
369
|
+
if (path === "/orders/order-1") return Promise.resolve({ data: completeFor("bill-1") });
|
|
370
|
+
if (path === "/orders/order-2") return Promise.resolve({ data: completeFor("bill-2") });
|
|
371
|
+
if (path === "/users/seller-1") return Promise.resolve({ data: { site_id: "MLA" } });
|
|
372
|
+
if (path === "/orders/billing-info/MLA/bill-1" || path === "/orders/billing-info/MLA/bill-2") return Promise.resolve({ data: completeBillingInfo() });
|
|
373
|
+
throw new Error(`unexpected path ${path}`);
|
|
374
|
+
});
|
|
375
|
+
const result = await getMercadoLibreSaleBillingInfo("triplim", "pack-1", { maxConcurrency: 2 });
|
|
376
|
+
expect(result).toMatchObject({
|
|
377
|
+
resolutionKind: "pack",
|
|
378
|
+
packId: "pack-1",
|
|
379
|
+
metadata: { memberCount: 2, accessibleCount: 2, availableCount: 2, unavailableCount: 0, failedCount: 0, partialCoverage: false },
|
|
380
|
+
members: [expect.objectContaining({ orderId: "order-1", accessible: true }), expect.objectContaining({ orderId: "order-2", accessible: true })]
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
it("keeps pack member failures and ownership mismatches scoped to their members", async () => {
|
|
384
|
+
getMock.mockImplementation((path) => {
|
|
385
|
+
if (path === "/orders/pack-1") return Promise.reject(new MercadoLibreApiError("not found", 404));
|
|
386
|
+
if (path === "/packs/pack-1") return Promise.resolve({ data: { orders: [{ id: "foreign" }, { id: "missing" }] } });
|
|
387
|
+
if (path === "/orders/foreign") return Promise.resolve({ data: { seller: { id: "other-seller" }, buyer: { billing_info: { id: "do-not-read" } } } });
|
|
388
|
+
if (path === "/orders/missing") return Promise.reject(new MercadoLibreApiError("missing", 404));
|
|
389
|
+
throw new Error(`unexpected path ${path}`);
|
|
390
|
+
});
|
|
391
|
+
await expect(getMercadoLibreSaleBillingInfo("triplim", "pack-1")).resolves.toMatchObject({
|
|
392
|
+
resolutionKind: "pack",
|
|
393
|
+
metadata: { memberCount: 2, accessibleCount: 1, availableCount: 0, unavailableCount: 1, failedCount: 1, partialCoverage: true },
|
|
394
|
+
members: [
|
|
395
|
+
{ orderId: "foreign", accessible: true, result: { available: false, reason: "order_seller_ownership_mismatch" } },
|
|
396
|
+
{ orderId: "missing", accessible: false, failureReason: "order_not_found" }
|
|
397
|
+
]
|
|
398
|
+
});
|
|
399
|
+
expect(getMock).not.toHaveBeenCalledWith(expect.stringContaining("billing-info"));
|
|
400
|
+
});
|
|
401
|
+
it("keeps valid pack members when other pack entries do not contain an order ID", async () => {
|
|
402
|
+
getMock.mockImplementation((path) => {
|
|
403
|
+
if (path === "/orders/pack-1") return Promise.reject(new MercadoLibreApiError("not found", 404));
|
|
404
|
+
if (path === "/packs/pack-1") return Promise.resolve({ data: { orders: [{ id: "order-1" }, {}, { id: " " }] } });
|
|
405
|
+
if (path === "/orders/order-1") return Promise.resolve({ data: { seller: { id: "seller-1" }, buyer: { billing_info: { id: "bill-1" } } } });
|
|
406
|
+
if (path === "/users/seller-1") return Promise.resolve({ data: { site_id: "MLA" } });
|
|
407
|
+
if (path === "/orders/billing-info/MLA/bill-1") return Promise.resolve({ data: completeBillingInfo() });
|
|
408
|
+
throw new Error(`unexpected path ${path}`);
|
|
409
|
+
});
|
|
410
|
+
await expect(getMercadoLibreSaleBillingInfo("triplim", "pack-1")).resolves.toMatchObject({
|
|
411
|
+
resolutionKind: "pack",
|
|
412
|
+
metadata: {
|
|
413
|
+
memberCount: 3,
|
|
414
|
+
validMemberCount: 1,
|
|
415
|
+
invalidMemberCount: 2,
|
|
416
|
+
accessibleCount: 1,
|
|
417
|
+
availableCount: 1,
|
|
418
|
+
unavailableCount: 0,
|
|
419
|
+
failedCount: 2,
|
|
420
|
+
partialCoverage: true
|
|
421
|
+
},
|
|
422
|
+
members: [
|
|
423
|
+
{ orderId: "order-1", accessible: true, result: { available: true } },
|
|
424
|
+
{ memberIndex: 1, accessible: false, failureReason: "invalid_member" },
|
|
425
|
+
{ memberIndex: 2, accessible: false, failureReason: "invalid_member" }
|
|
426
|
+
]
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
it("keeps pack member resolution at the configured concurrency ceiling", async () => {
|
|
430
|
+
let activeMembers = 0;
|
|
431
|
+
let maxActiveMembers = 0;
|
|
432
|
+
const firstWaveStarted = createDeferred();
|
|
433
|
+
const thirdMemberStarted = createDeferred();
|
|
434
|
+
const fourthMemberStarted = createDeferred();
|
|
435
|
+
const memberRequests = /* @__PURE__ */ new Map();
|
|
436
|
+
getMock.mockImplementation((path) => {
|
|
437
|
+
if (path === "/orders/pack-1") return Promise.reject(new MercadoLibreApiError("not found", 404));
|
|
438
|
+
if (path === "/packs/pack-1") return Promise.resolve({ data: { orders: [{ id: "one" }, { id: "two" }, { id: "three" }, { id: "four" }] } });
|
|
439
|
+
if (path.startsWith("/orders/") && !path.startsWith("/orders/billing-info/")) {
|
|
440
|
+
activeMembers += 1;
|
|
441
|
+
maxActiveMembers = Math.max(maxActiveMembers, activeMembers);
|
|
442
|
+
if (activeMembers === 2) firstWaveStarted.resolve();
|
|
443
|
+
const request = createDeferred();
|
|
444
|
+
memberRequests.set(path, request);
|
|
445
|
+
if (memberRequests.size === 3) thirdMemberStarted.resolve();
|
|
446
|
+
if (memberRequests.size === 4) fourthMemberStarted.resolve();
|
|
447
|
+
return request.promise.finally(() => {
|
|
448
|
+
activeMembers -= 1;
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
throw new Error(`unexpected path ${path}`);
|
|
452
|
+
});
|
|
453
|
+
const result = getMercadoLibreSaleBillingInfo("triplim", "pack-1", { maxConcurrency: 2 });
|
|
454
|
+
await firstWaveStarted.promise;
|
|
455
|
+
expect(maxActiveMembers).toBe(2);
|
|
456
|
+
memberRequests.get("/orders/one").resolve({ data: { seller: { id: "seller-1" }, buyer: {} } });
|
|
457
|
+
await thirdMemberStarted.promise;
|
|
458
|
+
expect(maxActiveMembers).toBe(2);
|
|
459
|
+
for (const path of ["/orders/two", "/orders/three"]) {
|
|
460
|
+
memberRequests.get(path).resolve({ data: { seller: { id: "seller-1" }, buyer: {} } });
|
|
461
|
+
}
|
|
462
|
+
await fourthMemberStarted.promise;
|
|
463
|
+
memberRequests.get("/orders/four").resolve({ data: { seller: { id: "seller-1" }, buyer: {} } });
|
|
464
|
+
await result;
|
|
465
|
+
expect(maxActiveMembers).toBe(2);
|
|
466
|
+
});
|
|
467
|
+
it("reports a retry-exhausted member without losing pack aggregate coverage", async () => {
|
|
468
|
+
const pause = vi.fn().mockResolvedValue(void 0);
|
|
469
|
+
getMock.mockImplementation((path) => {
|
|
470
|
+
if (path === "/orders/pack-1") return Promise.reject(new MercadoLibreApiError("not found", 404));
|
|
471
|
+
if (path === "/packs/pack-1") return Promise.resolve({ data: { orders: [{ id: "unavailable" }] } });
|
|
472
|
+
if (path === "/orders/unavailable") {
|
|
473
|
+
return Promise.reject(new MercadoLibreApiError("buyer-tax-id-should-not-leak https://api.example/orders/unavailable", 503));
|
|
474
|
+
}
|
|
475
|
+
throw new Error(`unexpected path ${path}`);
|
|
476
|
+
});
|
|
477
|
+
const result = await getMercadoLibreSaleBillingInfo("triplim", "pack-1", { maxRetries: 1, sleep: pause });
|
|
478
|
+
expect(result).toMatchObject({
|
|
479
|
+
resolutionKind: "pack",
|
|
480
|
+
metadata: {
|
|
481
|
+
memberCount: 1,
|
|
482
|
+
accessibleCount: 0,
|
|
483
|
+
availableCount: 0,
|
|
484
|
+
unavailableCount: 0,
|
|
485
|
+
failedCount: 1,
|
|
486
|
+
partialCoverage: true
|
|
487
|
+
},
|
|
488
|
+
members: [{ orderId: "unavailable", accessible: false, failureReason: "upstream_unavailable", attempts: 2, retryable: true }]
|
|
489
|
+
});
|
|
490
|
+
expect(JSON.stringify(result)).not.toContain("buyer-tax-id-should-not-leak");
|
|
491
|
+
expect(JSON.stringify(result)).not.toContain("api.example");
|
|
492
|
+
expect(pause).toHaveBeenCalledTimes(1);
|
|
493
|
+
expect(getMock).toHaveBeenCalledWith("/orders/unavailable", expect.anything());
|
|
494
|
+
expect(getMock.mock.calls.filter(([path]) => path === "/orders/unavailable")).toHaveLength(2);
|
|
495
|
+
});
|
|
496
|
+
it("stops member retries when the feature-scoped pack deadline is exhausted", async () => {
|
|
497
|
+
const pause = vi.fn().mockResolvedValue(void 0);
|
|
498
|
+
getMock.mockImplementation((path) => {
|
|
499
|
+
if (path === "/orders/pack-1") return Promise.reject(new MercadoLibreApiError("not found", 404));
|
|
500
|
+
if (path === "/packs/pack-1") return Promise.resolve({ data: { orders: [{ id: "slow" }] } });
|
|
501
|
+
if (path === "/orders/slow") return Promise.reject(new MercadoLibreApiError("temporary", 503));
|
|
502
|
+
throw new Error(`unexpected path ${path}`);
|
|
503
|
+
});
|
|
504
|
+
await expect(
|
|
505
|
+
getMercadoLibreSaleBillingInfo("triplim", "pack-1", {
|
|
506
|
+
maxRetries: 2,
|
|
507
|
+
baseRetryDelayMs: 10,
|
|
508
|
+
packOperationDeadlineMs: 5,
|
|
509
|
+
now: () => 0,
|
|
510
|
+
sleep: pause
|
|
511
|
+
})
|
|
512
|
+
).resolves.toMatchObject({
|
|
513
|
+
resolutionKind: "pack",
|
|
514
|
+
metadata: { failedCount: 1, partialCoverage: true, deadlineExceeded: true },
|
|
515
|
+
members: [{ orderId: "slow", accessible: false, failureReason: "operation_deadline_exceeded" }]
|
|
516
|
+
});
|
|
517
|
+
expect(pause).not.toHaveBeenCalled();
|
|
518
|
+
expect(getMock.mock.calls.filter(([path]) => path === "/orders/slow")).toHaveLength(1);
|
|
519
|
+
});
|
|
520
|
+
it("keeps an initial order deadline unavailable and does not fall back after it expires", async () => {
|
|
521
|
+
const initialOrder = createDeferred();
|
|
522
|
+
const initialRequestStarted = createDeferred();
|
|
523
|
+
let now = 0;
|
|
524
|
+
getMock.mockImplementation((path) => {
|
|
525
|
+
if (path === "/orders/pack-1") {
|
|
526
|
+
initialRequestStarted.resolve();
|
|
527
|
+
return initialOrder.promise;
|
|
528
|
+
}
|
|
529
|
+
throw new Error(`unexpected path ${path}`);
|
|
530
|
+
});
|
|
531
|
+
const result = getMercadoLibreSaleBillingInfo("triplim", "pack-1", {
|
|
532
|
+
packOperationDeadlineMs: 5,
|
|
533
|
+
now: () => now
|
|
534
|
+
});
|
|
535
|
+
await initialRequestStarted.promise;
|
|
536
|
+
now = 5;
|
|
537
|
+
await expect(result).rejects.toThrow("MercadoLibre pack billing info operation deadline exceeded");
|
|
538
|
+
expect(getMock).toHaveBeenCalledWith("/orders/pack-1", expect.objectContaining({ signal: expect.any(AbortSignal) }));
|
|
539
|
+
expect(getMock).not.toHaveBeenCalledWith("/packs/pack-1", expect.anything());
|
|
540
|
+
});
|
|
541
|
+
it("does not start fallback pack resolution after the initial order lookup exhausts the shared deadline", async () => {
|
|
542
|
+
let now = 0;
|
|
543
|
+
getMock.mockImplementation((path) => {
|
|
544
|
+
if (path === "/orders/pack-1") {
|
|
545
|
+
now = 5;
|
|
546
|
+
return Promise.reject(new MercadoLibreApiError("not found", 404));
|
|
547
|
+
}
|
|
548
|
+
throw new Error(`unexpected path ${path}`);
|
|
549
|
+
});
|
|
550
|
+
await expect(
|
|
551
|
+
getMercadoLibreSaleBillingInfo("triplim", "pack-1", {
|
|
552
|
+
packOperationDeadlineMs: 5,
|
|
553
|
+
now: () => now
|
|
554
|
+
})
|
|
555
|
+
).resolves.toMatchObject({
|
|
556
|
+
resolutionKind: "pack",
|
|
557
|
+
unavailableReason: "pack_operation_deadline_exceeded",
|
|
558
|
+
metadata: { deadlineExceeded: true, partialCoverage: true },
|
|
559
|
+
members: []
|
|
560
|
+
});
|
|
561
|
+
expect(getMock).toHaveBeenCalledTimes(1);
|
|
562
|
+
expect(getMock).not.toHaveBeenCalledWith("/packs/pack-1", expect.anything());
|
|
563
|
+
});
|
|
564
|
+
it.each([
|
|
565
|
+
["empty", { orders: [] }, "pack_orders_empty"],
|
|
566
|
+
["malformed envelope", { orders: null }, "pack_payload_malformed"]
|
|
567
|
+
])("fails safely for a %s pack payload", async (_description, payload, reason) => {
|
|
568
|
+
getMock.mockRejectedValueOnce(new MercadoLibreApiError("not found", 404)).mockResolvedValueOnce({ data: payload });
|
|
569
|
+
await expect(getMercadoLibreSaleBillingInfo("triplim", "pack-1")).resolves.toMatchObject({
|
|
570
|
+
resolutionKind: "pack",
|
|
571
|
+
members: [],
|
|
572
|
+
unavailableReason: reason,
|
|
573
|
+
metadata: { partialCoverage: true }
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
it("does not attempt pack resolution for an order permission failure", async () => {
|
|
577
|
+
getMock.mockRejectedValueOnce(new MercadoLibreApiError("forbidden", 403));
|
|
578
|
+
await expect(getMercadoLibreSaleBillingInfo("triplim", "reference-1")).rejects.toMatchObject({ status: 403, resource: "order" });
|
|
579
|
+
expect(getMock).not.toHaveBeenCalledWith("/packs/reference-1");
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
//# sourceMappingURL=mercadolibre-order-billing-info.test.js.map
|