@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.
Files changed (158) hide show
  1. package/README.md +12 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/index.js +179 -6
  4. package/dist/index.js.map +2 -2
  5. package/dist/mcp-use.json +3 -6
  6. package/dist/src/config/__tests__/google-store.test.js +30 -0
  7. package/dist/src/config/__tests__/google-store.test.js.map +7 -0
  8. package/dist/src/config/__tests__/mcp-tool-deadline.test.js +64 -0
  9. package/dist/src/config/__tests__/mcp-tool-deadline.test.js.map +7 -0
  10. package/dist/src/config/google-store.js +18 -0
  11. package/dist/src/config/google-store.js.map +2 -2
  12. package/dist/src/config/mcp-tool-deadline-context.js +34 -0
  13. package/dist/src/config/mcp-tool-deadline-context.js.map +7 -0
  14. package/dist/src/config/mcp-tool-deadline.js +67 -0
  15. package/dist/src/config/mcp-tool-deadline.js.map +7 -0
  16. package/dist/src/services/mercadolibre/__tests__/mercadolibre-api.test.js +38 -3
  17. package/dist/src/services/mercadolibre/__tests__/mercadolibre-api.test.js.map +2 -2
  18. package/dist/src/services/mercadolibre/__tests__/mercadolibre-buyer-reconciliation.test.js +226 -0
  19. package/dist/src/services/mercadolibre/__tests__/mercadolibre-buyer-reconciliation.test.js.map +7 -0
  20. package/dist/src/services/mercadolibre/__tests__/mercadolibre-claim-reconciliation.test.js +127 -0
  21. package/dist/src/services/mercadolibre/__tests__/mercadolibre-claim-reconciliation.test.js.map +7 -0
  22. package/dist/src/services/mercadolibre/__tests__/mercadolibre-kits.test.js +103 -0
  23. package/dist/src/services/mercadolibre/__tests__/mercadolibre-kits.test.js.map +7 -0
  24. package/dist/src/services/mercadolibre/__tests__/mercadolibre-order-billing-info.test.js +582 -0
  25. package/dist/src/services/mercadolibre/__tests__/mercadolibre-order-billing-info.test.js.map +7 -0
  26. package/dist/src/services/mercadolibre/__tests__/mercadolibre-orders.test.js +10 -0
  27. package/dist/src/services/mercadolibre/__tests__/mercadolibre-orders.test.js.map +2 -2
  28. package/dist/src/services/mercadolibre/__tests__/mercadolibre-packs.test.js +36 -0
  29. package/dist/src/services/mercadolibre/__tests__/mercadolibre-packs.test.js.map +7 -0
  30. package/dist/src/services/mercadolibre/__tests__/mercadolibre-payments.test.js +137 -0
  31. package/dist/src/services/mercadolibre/__tests__/mercadolibre-payments.test.js.map +7 -0
  32. package/dist/src/services/mercadolibre/__tests__/mercadolibre-product-ads-ad-group-ads.test.js +105 -0
  33. package/dist/src/services/mercadolibre/__tests__/mercadolibre-product-ads-ad-group-ads.test.js.map +7 -0
  34. package/dist/src/services/mercadolibre/__tests__/mercadolibre-product-ads-ad-groups.test.js +143 -0
  35. package/dist/src/services/mercadolibre/__tests__/mercadolibre-product-ads-ad-groups.test.js.map +7 -0
  36. package/dist/src/services/mercadolibre/__tests__/mercadolibre-product-ads-campaigns.test.js +100 -0
  37. package/dist/src/services/mercadolibre/__tests__/mercadolibre-product-ads-campaigns.test.js.map +7 -0
  38. package/dist/src/services/mercadolibre/__tests__/mercadolibre-product-ads.test.js +350 -0
  39. package/dist/src/services/mercadolibre/__tests__/mercadolibre-product-ads.test.js.map +7 -0
  40. package/dist/src/services/mercadolibre/__tests__/mercadolibre-return-reconciliation.test.js +228 -0
  41. package/dist/src/services/mercadolibre/__tests__/mercadolibre-return-reconciliation.test.js.map +7 -0
  42. package/dist/src/services/mercadolibre/__tests__/mercadolibre-shipping.test.js +58 -0
  43. package/dist/src/services/mercadolibre/__tests__/mercadolibre-shipping.test.js.map +7 -0
  44. package/dist/src/services/mercadolibre/mercadolibre-api.js +15 -3
  45. package/dist/src/services/mercadolibre/mercadolibre-api.js.map +2 -2
  46. package/dist/src/services/mercadolibre/mercadolibre-billing.js +110 -3
  47. package/dist/src/services/mercadolibre/mercadolibre-billing.js.map +2 -2
  48. package/dist/src/services/mercadolibre/mercadolibre-buyer-reconciliation.js +368 -0
  49. package/dist/src/services/mercadolibre/mercadolibre-buyer-reconciliation.js.map +7 -0
  50. package/dist/src/services/mercadolibre/mercadolibre-claim-reconciliation.js +399 -0
  51. package/dist/src/services/mercadolibre/mercadolibre-claim-reconciliation.js.map +7 -0
  52. package/dist/src/services/mercadolibre/mercadolibre-kits.js +55 -0
  53. package/dist/src/services/mercadolibre/mercadolibre-kits.js.map +7 -0
  54. package/dist/src/services/mercadolibre/mercadolibre-order-billing-info.js +392 -0
  55. package/dist/src/services/mercadolibre/mercadolibre-order-billing-info.js.map +7 -0
  56. package/dist/src/services/mercadolibre/mercadolibre-orders.js +8 -8
  57. package/dist/src/services/mercadolibre/mercadolibre-orders.js.map +2 -2
  58. package/dist/src/services/mercadolibre/mercadolibre-packs.js +21 -0
  59. package/dist/src/services/mercadolibre/mercadolibre-packs.js.map +7 -0
  60. package/dist/src/services/mercadolibre/mercadolibre-payments.js +60 -0
  61. package/dist/src/services/mercadolibre/mercadolibre-payments.js.map +7 -0
  62. package/dist/src/services/mercadolibre/mercadolibre-product-ads.js +429 -0
  63. package/dist/src/services/mercadolibre/mercadolibre-product-ads.js.map +7 -0
  64. package/dist/src/services/mercadolibre/mercadolibre-return-reconciliation.js +436 -0
  65. package/dist/src/services/mercadolibre/mercadolibre-return-reconciliation.js.map +7 -0
  66. package/dist/src/services/mercadolibre/mercadolibre-shipping.js +35 -0
  67. package/dist/src/services/mercadolibre/mercadolibre-shipping.js.map +7 -0
  68. package/dist/src/services/vtex/__tests__/vtex-api.test.js +67 -0
  69. package/dist/src/services/vtex/__tests__/vtex-api.test.js.map +7 -0
  70. package/dist/src/services/vtex/vtex-api.js +6 -0
  71. package/dist/src/services/vtex/vtex-api.js.map +2 -2
  72. package/dist/src/tools/config/__tests__/list-profiles.test.js +154 -0
  73. package/dist/src/tools/config/__tests__/list-profiles.test.js.map +7 -0
  74. package/dist/src/tools/config/list-profiles.js +39 -3
  75. package/dist/src/tools/config/list-profiles.js.map +2 -2
  76. package/dist/src/tools/mercadolibre/__tests__/get-order-billing-info.test.js +339 -0
  77. package/dist/src/tools/mercadolibre/__tests__/get-order-billing-info.test.js.map +7 -0
  78. package/dist/src/tools/mercadolibre/__tests__/product-ads-ad-group-ads.test.js +225 -0
  79. package/dist/src/tools/mercadolibre/__tests__/product-ads-ad-group-ads.test.js.map +7 -0
  80. package/dist/src/tools/mercadolibre/__tests__/product-ads-ad-groups.test.js +148 -0
  81. package/dist/src/tools/mercadolibre/__tests__/product-ads-ad-groups.test.js.map +7 -0
  82. package/dist/src/tools/mercadolibre/__tests__/product-ads-campaigns.test.js +60 -0
  83. package/dist/src/tools/mercadolibre/__tests__/product-ads-campaigns.test.js.map +7 -0
  84. package/dist/src/tools/mercadolibre/__tests__/product-ads-helpers.test.js +128 -0
  85. package/dist/src/tools/mercadolibre/__tests__/product-ads-helpers.test.js.map +7 -0
  86. package/dist/src/tools/mercadolibre/__tests__/product-ads-tools.test.js +435 -0
  87. package/dist/src/tools/mercadolibre/__tests__/product-ads-tools.test.js.map +7 -0
  88. package/dist/src/tools/mercadolibre/__tests__/reconcile-buyer-data.test.js +31 -0
  89. package/dist/src/tools/mercadolibre/__tests__/reconcile-buyer-data.test.js.map +7 -0
  90. package/dist/src/tools/mercadolibre/__tests__/reconcile-claim-data.test.js +36 -0
  91. package/dist/src/tools/mercadolibre/__tests__/reconcile-claim-data.test.js.map +7 -0
  92. package/dist/src/tools/mercadolibre/__tests__/validate-connection.test.js +205 -0
  93. package/dist/src/tools/mercadolibre/__tests__/validate-connection.test.js.map +7 -0
  94. package/dist/src/tools/mercadolibre/get-billing-provisions-by-order.js +313 -0
  95. package/dist/src/tools/mercadolibre/get-billing-provisions-by-order.js.map +7 -0
  96. package/dist/src/tools/mercadolibre/get-bundle-context-by-item.js +176 -0
  97. package/dist/src/tools/mercadolibre/get-bundle-context-by-item.js.map +7 -0
  98. package/dist/src/tools/mercadolibre/get-full-storage-charges.js +183 -0
  99. package/dist/src/tools/mercadolibre/get-full-storage-charges.js.map +7 -0
  100. package/dist/src/tools/mercadolibre/get-order-billing-info.js +189 -0
  101. package/dist/src/tools/mercadolibre/get-order-billing-info.js.map +7 -0
  102. package/dist/src/tools/mercadolibre/get-pack-context-by-order.js +150 -0
  103. package/dist/src/tools/mercadolibre/get-pack-context-by-order.js.map +7 -0
  104. package/dist/src/tools/mercadolibre/get-payment-details.js +322 -0
  105. package/dist/src/tools/mercadolibre/get-payment-details.js.map +7 -0
  106. package/dist/src/tools/mercadolibre/get-product-ads-ad-group-ads.js +144 -0
  107. package/dist/src/tools/mercadolibre/get-product-ads-ad-group-ads.js.map +7 -0
  108. package/dist/src/tools/mercadolibre/get-product-ads-advertisers.js +59 -0
  109. package/dist/src/tools/mercadolibre/get-product-ads-advertisers.js.map +7 -0
  110. package/dist/src/tools/mercadolibre/get-shipping-quote-by-declared-dimensions.js +104 -0
  111. package/dist/src/tools/mercadolibre/get-shipping-quote-by-declared-dimensions.js.map +7 -0
  112. package/dist/src/tools/mercadolibre/index.js +32 -0
  113. package/dist/src/tools/mercadolibre/index.js.map +2 -2
  114. package/dist/src/tools/mercadolibre/product-ads-helpers.js +130 -0
  115. package/dist/src/tools/mercadolibre/product-ads-helpers.js.map +7 -0
  116. package/dist/src/tools/mercadolibre/reconcile-buyer-data.js +27 -0
  117. package/dist/src/tools/mercadolibre/reconcile-buyer-data.js.map +7 -0
  118. package/dist/src/tools/mercadolibre/reconcile-claim-data.js +46 -0
  119. package/dist/src/tools/mercadolibre/reconcile-claim-data.js.map +7 -0
  120. package/dist/src/tools/mercadolibre/reconcile-return-data.js +25 -0
  121. package/dist/src/tools/mercadolibre/reconcile-return-data.js.map +7 -0
  122. package/dist/src/tools/mercadolibre/search-product-ads-ad-groups.js +162 -0
  123. package/dist/src/tools/mercadolibre/search-product-ads-ad-groups.js.map +7 -0
  124. package/dist/src/tools/mercadolibre/search-product-ads-ads.js +216 -0
  125. package/dist/src/tools/mercadolibre/search-product-ads-ads.js.map +7 -0
  126. package/dist/src/tools/mercadolibre/search-product-ads-campaigns.js +116 -0
  127. package/dist/src/tools/mercadolibre/search-product-ads-campaigns.js.map +7 -0
  128. package/dist/src/tools/mercadolibre/validate-connection.js +40 -12
  129. package/dist/src/tools/mercadolibre/validate-connection.js.map +2 -2
  130. package/dist/tests/meli/mercadolibre-billing-provisions.test.js +378 -0
  131. package/dist/tests/meli/mercadolibre-billing-provisions.test.js.map +2 -2
  132. package/dist/tests/meli/mercadolibre-order-billing-info-surface.test.js +123 -0
  133. package/dist/tests/meli/mercadolibre-order-billing-info-surface.test.js.map +7 -0
  134. package/dist/tests/meli/mercadolibre-payment-details.test.js +356 -0
  135. package/dist/tests/meli/mercadolibre-payment-details.test.js.map +7 -0
  136. package/dist/tests/meli/mercadolibre-payments.test.js +62 -0
  137. package/dist/tests/meli/mercadolibre-payments.test.js.map +7 -0
  138. package/dist/tests/meli/mercadolibre-product-ads-ad-groups-surface.test.js +90 -0
  139. package/dist/tests/meli/mercadolibre-product-ads-ad-groups-surface.test.js.map +7 -0
  140. package/dist/tests/meli/mercadolibre-product-ads-campaigns-surface.test.js +72 -0
  141. package/dist/tests/meli/mercadolibre-product-ads-campaigns-surface.test.js.map +7 -0
  142. package/dist/tests/meli/mercadolibre-tool-handlers-pack-kit-shipping.test.js +400 -0
  143. package/dist/tests/meli/mercadolibre-tool-handlers-pack-kit-shipping.test.js.map +7 -0
  144. package/dist/tests/meli/mercadolibre-tool-handlers-small.test.js +31 -12
  145. package/dist/tests/meli/mercadolibre-tool-handlers-small.test.js.map +2 -2
  146. package/dist/tests/vtex/vtex-commercial-conditions-docs.test.js +1 -1
  147. package/dist/tests/vtex/vtex-commercial-conditions-docs.test.js.map +1 -1
  148. package/package.json +1 -1
  149. package/dist/src/google-ads/report-utils.js +0 -78
  150. package/dist/src/google-ads/report-utils.js.map +0 -7
  151. package/dist/src/search-console/search-console-utils.js +0 -275
  152. package/dist/src/search-console/search-console-utils.js.map +0 -7
  153. package/dist/src/services/vtex/__tests__/vtex-catalog-write-batch.test.js +0 -165
  154. package/dist/src/services/vtex/__tests__/vtex-catalog-write-batch.test.js.map +0 -7
  155. package/dist/src/tools/vtex/__tests__/catalog-admin-batch.test.js +0 -233
  156. package/dist/src/tools/vtex/__tests__/catalog-admin-batch.test.js.map +0 -7
  157. package/dist/src/tools/vtex/catalog-admin-batch.js +0 -298
  158. package/dist/src/tools/vtex/catalog-admin-batch.js.map +0 -7
@@ -0,0 +1,143 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { MercadoLibreApiError, getMercadoLibreApiContext } from "../mercadolibre-api.js";
3
+ import { searchMercadoLibreProductAdsAdGroups } from "../mercadolibre-product-ads.js";
4
+ vi.mock("../mercadolibre-api.js", async () => {
5
+ const actual = await vi.importActual("../mercadolibre-api.js");
6
+ return { ...actual, getMercadoLibreApiContext: vi.fn() };
7
+ });
8
+ describe("mercadolibre product ads ad groups service", () => {
9
+ const getMock = vi.fn();
10
+ beforeEach(() => {
11
+ vi.clearAllMocks();
12
+ vi.mocked(getMercadoLibreApiContext).mockResolvedValue({
13
+ api: { get: getMock },
14
+ sellerId: "seller-1",
15
+ accessToken: "token-1"
16
+ });
17
+ });
18
+ it("uses the validated ad-groups search path, query contract, and Product Ads API version", async () => {
19
+ getMock.mockResolvedValueOnce({
20
+ data: {
21
+ paging: { total: 11, limit: 25, offset: 10 },
22
+ results: [{ id: "ag-1", campaign_id: "campaign-1", metrics: { clicks: 3 } }],
23
+ metrics_summary: { clicks: 3 }
24
+ }
25
+ });
26
+ await expect(
27
+ searchMercadoLibreProductAdsAdGroups("profile-1", {
28
+ siteId: "MLA",
29
+ advertiserId: "89015",
30
+ dateFrom: "2026-06-01",
31
+ dateTo: "2026-06-30",
32
+ metrics: ["clicks", "cost"],
33
+ metricsSummary: true,
34
+ sort: "desc",
35
+ sortBy: "clicks",
36
+ filters: { campaigns: "campaign-1", statuses: "active" },
37
+ limit: 25,
38
+ offset: 10
39
+ })
40
+ ).resolves.toEqual({
41
+ paging: { total: 11, limit: 25, offset: 10 },
42
+ results: [{ id: "ag-1", campaign_id: "campaign-1", metrics: { clicks: 3 } }],
43
+ metricsSummary: { clicks: 3 }
44
+ });
45
+ expect(getMock).toHaveBeenCalledWith(
46
+ "/advertising/MLA/advertisers/89015/product_ads/ad_groups/search",
47
+ {
48
+ params: {
49
+ date_from: "2026-06-01",
50
+ date_to: "2026-06-30",
51
+ metrics: "clicks,cost",
52
+ metrics_summary: true,
53
+ sort: "desc",
54
+ sort_by: "clicks",
55
+ "filters[campaigns]": "campaign-1",
56
+ "filters[statuses]": "active",
57
+ limit: 25,
58
+ offset: 10
59
+ },
60
+ headers: { "Api-Version": "v2" }
61
+ }
62
+ );
63
+ });
64
+ it("preserves the validated empty state without fabricating rows", async () => {
65
+ getMock.mockResolvedValueOnce({
66
+ data: { paging: { total: 0, limit: 50, offset: 0 }, results: [] }
67
+ });
68
+ await expect(
69
+ searchMercadoLibreProductAdsAdGroups("profile-1", {
70
+ siteId: "MLA",
71
+ advertiserId: "89015",
72
+ dateFrom: "2026-06-01",
73
+ dateTo: "2026-06-30"
74
+ })
75
+ ).resolves.toEqual({
76
+ paging: { total: 0, limit: 50, offset: 0 },
77
+ results: [],
78
+ metricsSummary: void 0
79
+ });
80
+ });
81
+ it.each([
82
+ { paging: {}, reason: "missing" },
83
+ { paging: { total: null, limit: null, offset: null }, reason: "null" },
84
+ { paging: { total: 2, limit: "not-a-number", offset: 0 }, reason: "invalid" },
85
+ { paging: { total: 2, limit: 0, offset: 0 }, reason: "zero-limit" },
86
+ { paging: { total: 2, limit: -1, offset: 0 }, reason: "negative-limit" }
87
+ ])("rejects $reason paging metadata instead of fabricating continuation", async ({ paging }) => {
88
+ getMock.mockResolvedValueOnce({ data: { paging, results: [] } });
89
+ await expect(
90
+ searchMercadoLibreProductAdsAdGroups("profile-1", {
91
+ siteId: "MLA",
92
+ advertiserId: "89015",
93
+ dateFrom: "2026-06-01",
94
+ dateTo: "2026-06-30"
95
+ })
96
+ ).rejects.toThrow("paging metadata; continuation cannot be determined safely");
97
+ });
98
+ it.each([
99
+ { results: void 0, reason: "missing" },
100
+ { results: {}, reason: "non-array" }
101
+ ])("rejects $reason results when paging reports data", async ({ results }) => {
102
+ getMock.mockResolvedValueOnce({
103
+ data: { paging: { total: 2, limit: 50, offset: 0 }, ...results === void 0 ? {} : { results } }
104
+ });
105
+ await expect(
106
+ searchMercadoLibreProductAdsAdGroups("profile-1", {
107
+ siteId: "MLA",
108
+ advertiserId: "89015",
109
+ dateFrom: "2026-06-01",
110
+ dateTo: "2026-06-30"
111
+ })
112
+ ).rejects.toThrow("missing or invalid results; page integrity cannot be determined safely");
113
+ });
114
+ it("rejects an underfilled page instead of advertising an unsafe continuation", async () => {
115
+ getMock.mockResolvedValueOnce({
116
+ data: {
117
+ paging: { total: 120, limit: 50, offset: 0 },
118
+ results: [{ id: "ag-1" }]
119
+ }
120
+ });
121
+ await expect(
122
+ searchMercadoLibreProductAdsAdGroups("profile-1", {
123
+ siteId: "MLA",
124
+ advertiserId: "89015",
125
+ dateFrom: "2026-06-01",
126
+ dateTo: "2026-06-30"
127
+ })
128
+ ).rejects.toThrow("underfilled or oversized page; continuation cannot be determined safely");
129
+ });
130
+ it.each([401, 403, 404])("preserves undocumented upstream %s errors", async (status) => {
131
+ getMock.mockRejectedValueOnce(new MercadoLibreApiError(`upstream ${status}`, status));
132
+ await expect(
133
+ searchMercadoLibreProductAdsAdGroups("profile-1", {
134
+ siteId: "MLA",
135
+ advertiserId: "89015",
136
+ dateFrom: "2026-06-01",
137
+ dateTo: "2026-06-30"
138
+ })
139
+ ).rejects.toMatchObject({ status, message: `upstream ${status}` });
140
+ expect(getMock).toHaveBeenCalledTimes(1);
141
+ });
142
+ });
143
+ //# sourceMappingURL=mercadolibre-product-ads-ad-groups.test.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/services/mercadolibre/__tests__/mercadolibre-product-ads-ad-groups.test.ts"],
4
+ "sourcesContent": ["import { beforeEach, describe, expect, it, vi } from \"vitest\";\n\nimport { MercadoLibreApiError, getMercadoLibreApiContext } from \"../mercadolibre-api.js\";\nimport { searchMercadoLibreProductAdsAdGroups } from \"../mercadolibre-product-ads.js\";\n\nvi.mock(\"../mercadolibre-api.js\", async () => {\n const actual = await vi.importActual<typeof import(\"../mercadolibre-api.js\")>(\"../mercadolibre-api.js\");\n\n return { ...actual, getMercadoLibreApiContext: vi.fn() };\n});\n\ndescribe(\"mercadolibre product ads ad groups service\", () => {\n const getMock = vi.fn();\n\n beforeEach(() => {\n vi.clearAllMocks();\n vi.mocked(getMercadoLibreApiContext).mockResolvedValue({\n api: { get: getMock },\n sellerId: \"seller-1\",\n accessToken: \"token-1\",\n } as never);\n });\n\n it(\"uses the validated ad-groups search path, query contract, and Product Ads API version\", async () => {\n getMock.mockResolvedValueOnce({\n data: {\n paging: { total: 11, limit: 25, offset: 10 },\n results: [{ id: \"ag-1\", campaign_id: \"campaign-1\", metrics: { clicks: 3 } }],\n metrics_summary: { clicks: 3 },\n },\n });\n\n await expect(\n searchMercadoLibreProductAdsAdGroups(\"profile-1\", {\n siteId: \"MLA\",\n advertiserId: \"89015\",\n dateFrom: \"2026-06-01\",\n dateTo: \"2026-06-30\",\n metrics: [\"clicks\", \"cost\"],\n metricsSummary: true,\n sort: \"desc\",\n sortBy: \"clicks\",\n filters: { campaigns: \"campaign-1\", statuses: \"active\" },\n limit: 25,\n offset: 10,\n })\n ).resolves.toEqual({\n paging: { total: 11, limit: 25, offset: 10 },\n results: [{ id: \"ag-1\", campaign_id: \"campaign-1\", metrics: { clicks: 3 } }],\n metricsSummary: { clicks: 3 },\n });\n\n expect(getMock).toHaveBeenCalledWith(\n \"/advertising/MLA/advertisers/89015/product_ads/ad_groups/search\",\n {\n params: {\n date_from: \"2026-06-01\",\n date_to: \"2026-06-30\",\n metrics: \"clicks,cost\",\n metrics_summary: true,\n sort: \"desc\",\n sort_by: \"clicks\",\n \"filters[campaigns]\": \"campaign-1\",\n \"filters[statuses]\": \"active\",\n limit: 25,\n offset: 10,\n },\n headers: { \"Api-Version\": \"v2\" },\n }\n );\n });\n\n it(\"preserves the validated empty state without fabricating rows\", async () => {\n getMock.mockResolvedValueOnce({\n data: { paging: { total: 0, limit: 50, offset: 0 }, results: [] },\n });\n\n await expect(\n searchMercadoLibreProductAdsAdGroups(\"profile-1\", {\n siteId: \"MLA\",\n advertiserId: \"89015\",\n dateFrom: \"2026-06-01\",\n dateTo: \"2026-06-30\",\n })\n ).resolves.toEqual({\n paging: { total: 0, limit: 50, offset: 0 },\n results: [],\n metricsSummary: undefined,\n });\n });\n\n it.each([\n { paging: {}, reason: \"missing\" },\n { paging: { total: null, limit: null, offset: null }, reason: \"null\" },\n { paging: { total: 2, limit: \"not-a-number\", offset: 0 }, reason: \"invalid\" },\n { paging: { total: 2, limit: 0, offset: 0 }, reason: \"zero-limit\" },\n { paging: { total: 2, limit: -1, offset: 0 }, reason: \"negative-limit\" },\n ])(\"rejects $reason paging metadata instead of fabricating continuation\", async ({ paging }) => {\n getMock.mockResolvedValueOnce({ data: { paging, results: [] } });\n\n await expect(\n searchMercadoLibreProductAdsAdGroups(\"profile-1\", {\n siteId: \"MLA\",\n advertiserId: \"89015\",\n dateFrom: \"2026-06-01\",\n dateTo: \"2026-06-30\",\n })\n ).rejects.toThrow(\"paging metadata; continuation cannot be determined safely\");\n });\n\n it.each([\n { results: undefined, reason: \"missing\" },\n { results: {}, reason: \"non-array\" },\n ])(\"rejects $reason results when paging reports data\", async ({ results }) => {\n getMock.mockResolvedValueOnce({\n data: { paging: { total: 2, limit: 50, offset: 0 }, ...(results === undefined ? {} : { results }) },\n });\n\n await expect(\n searchMercadoLibreProductAdsAdGroups(\"profile-1\", {\n siteId: \"MLA\",\n advertiserId: \"89015\",\n dateFrom: \"2026-06-01\",\n dateTo: \"2026-06-30\",\n })\n ).rejects.toThrow(\"missing or invalid results; page integrity cannot be determined safely\");\n });\n\n it(\"rejects an underfilled page instead of advertising an unsafe continuation\", async () => {\n getMock.mockResolvedValueOnce({\n data: {\n paging: { total: 120, limit: 50, offset: 0 },\n results: [{ id: \"ag-1\" }],\n },\n });\n\n await expect(\n searchMercadoLibreProductAdsAdGroups(\"profile-1\", {\n siteId: \"MLA\",\n advertiserId: \"89015\",\n dateFrom: \"2026-06-01\",\n dateTo: \"2026-06-30\",\n })\n ).rejects.toThrow(\"underfilled or oversized page; continuation cannot be determined safely\");\n });\n\n it.each([401, 403, 404])(\"preserves undocumented upstream %s errors\", async (status) => {\n getMock.mockRejectedValueOnce(new MercadoLibreApiError(`upstream ${status}`, status));\n\n await expect(\n searchMercadoLibreProductAdsAdGroups(\"profile-1\", {\n siteId: \"MLA\",\n advertiserId: \"89015\",\n dateFrom: \"2026-06-01\",\n dateTo: \"2026-06-30\",\n })\n ).rejects.toMatchObject({ status, message: `upstream ${status}` });\n\n expect(getMock).toHaveBeenCalledTimes(1);\n });\n});\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,100 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { MercadoLibreApiError, getMercadoLibreApiContext } from "../mercadolibre-api.js";
3
+ import { searchMercadoLibreProductAdsCampaigns } from "../mercadolibre-product-ads.js";
4
+ vi.mock("../mercadolibre-api.js", async () => {
5
+ const actual = await vi.importActual("../mercadolibre-api.js");
6
+ return { ...actual, getMercadoLibreApiContext: vi.fn() };
7
+ });
8
+ describe("mercadolibre product ads campaigns service", () => {
9
+ const getMock = vi.fn();
10
+ const params = { siteId: "MLA", advertiserId: "89015", dateFrom: "2026-06-01", dateTo: "2026-06-30" };
11
+ beforeEach(() => {
12
+ vi.clearAllMocks();
13
+ vi.mocked(getMercadoLibreApiContext).mockResolvedValue({ api: { get: getMock } });
14
+ });
15
+ it("uses the validated campaigns search path, page query, and api-version 2 header", async () => {
16
+ const results = [{ id: "campaign-1", name: "Campaign", budget: { amount: 1e3 } }];
17
+ getMock.mockResolvedValueOnce({ data: { paging: { total: 5, limit: 1, offset: 1 }, results } });
18
+ await expect(searchMercadoLibreProductAdsCampaigns("triplim", { ...params, limit: 1, offset: 1 })).resolves.toEqual({
19
+ paging: { total: 5, limit: 1, offset: 1 },
20
+ results
21
+ });
22
+ expect(getMock).toHaveBeenCalledWith(
23
+ "/advertising/MLA/advertisers/89015/product_ads/campaigns/search",
24
+ {
25
+ params: { date_from: "2026-06-01", date_to: "2026-06-30", limit: 1, offset: 1 },
26
+ headers: { "api-version": "2" },
27
+ timeout: 1e4
28
+ }
29
+ );
30
+ });
31
+ it.each([{ paging: {} }, { paging: { total: 5, limit: "bad", offset: 0 } }])(
32
+ "rejects malformed paging instead of fabricating continuation",
33
+ async ({ paging }) => {
34
+ getMock.mockResolvedValueOnce({ data: { paging, results: [] } });
35
+ await expect(searchMercadoLibreProductAdsCampaigns("triplim", params)).rejects.toThrow(
36
+ "campaigns response has missing or invalid paging metadata"
37
+ );
38
+ }
39
+ );
40
+ it.each([401, 403, 404])("preserves upstream %s errors", async (status) => {
41
+ getMock.mockRejectedValueOnce(new MercadoLibreApiError(`upstream ${status}`, status));
42
+ await expect(searchMercadoLibreProductAdsCampaigns("triplim", params)).rejects.toMatchObject({ status, message: `upstream ${status}` });
43
+ });
44
+ it("rejects a zero limit before it can create a false terminal page", async () => {
45
+ await expect(searchMercadoLibreProductAdsCampaigns("triplim", { ...params, limit: 0 })).rejects.toThrow(
46
+ "limit must be an integer between 1 and 100"
47
+ );
48
+ expect(getMock).not.toHaveBeenCalled();
49
+ });
50
+ it.each([{}, { results: void 0 }, { results: {} }, { results: "not-an-array" }])(
51
+ "rejects missing or malformed results instead of treating it as an empty page",
52
+ async (payload) => {
53
+ getMock.mockResolvedValueOnce({ data: { paging: { total: 0, limit: 1, offset: 0 }, ...payload } });
54
+ await expect(searchMercadoLibreProductAdsCampaigns("triplim", params)).rejects.toThrow(
55
+ "campaigns response has missing or invalid results"
56
+ );
57
+ }
58
+ );
59
+ it("rejects underfilled pages so continuation can never skip undiscovered campaigns", async () => {
60
+ const onObservation = vi.fn();
61
+ getMock.mockResolvedValueOnce({
62
+ data: { paging: { total: 5, limit: 2, offset: 0 }, results: [{ id: "campaign-1" }] }
63
+ });
64
+ await expect(searchMercadoLibreProductAdsCampaigns("triplim", params, { onObservation })).rejects.toThrow(
65
+ "underfilled or oversized page"
66
+ );
67
+ expect(onObservation).toHaveBeenCalledWith(expect.objectContaining({ event: "payload_integrity_failure" }));
68
+ });
69
+ it("stops retrying when the campaign discovery latency budget cannot cover the next backoff", async () => {
70
+ const onObservation = vi.fn();
71
+ getMock.mockRejectedValueOnce(new MercadoLibreApiError("temporarily unavailable", 503));
72
+ await expect(
73
+ searchMercadoLibreProductAdsCampaigns("triplim", params, {
74
+ totalBudgetMs: 1,
75
+ baseRetryDelayMs: 50,
76
+ onObservation
77
+ })
78
+ ).rejects.toMatchObject({ code: "campaign_discovery_budget_exhausted" });
79
+ expect(getMock).toHaveBeenCalledTimes(1);
80
+ expect(onObservation).toHaveBeenCalledWith(expect.objectContaining({ event: "failure", reason: "latency_budget_exhausted" }));
81
+ });
82
+ it("emits retry and retry-exhaustion observations with a bounded per-attempt timeout", async () => {
83
+ const onObservation = vi.fn();
84
+ getMock.mockRejectedValue(new MercadoLibreApiError("temporarily unavailable", 503));
85
+ await expect(
86
+ searchMercadoLibreProductAdsCampaigns("triplim", params, {
87
+ maxRetries: 1,
88
+ baseRetryDelayMs: 50,
89
+ totalBudgetMs: 1e3,
90
+ requestTimeoutMs: 123,
91
+ onObservation
92
+ })
93
+ ).rejects.toThrow("temporarily unavailable");
94
+ expect(getMock).toHaveBeenCalledTimes(2);
95
+ expect(getMock.mock.calls[0]?.[1]).toEqual(expect.objectContaining({ timeout: 123 }));
96
+ expect(onObservation).toHaveBeenCalledWith(expect.objectContaining({ event: "retry", attempt: 1, status: 503 }));
97
+ expect(onObservation).toHaveBeenCalledWith(expect.objectContaining({ event: "failure", attempt: 2, reason: "retry_exhausted" }));
98
+ });
99
+ });
100
+ //# sourceMappingURL=mercadolibre-product-ads-campaigns.test.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/services/mercadolibre/__tests__/mercadolibre-product-ads-campaigns.test.ts"],
4
+ "sourcesContent": ["import { beforeEach, describe, expect, it, vi } from \"vitest\";\n\nimport { MercadoLibreApiError, getMercadoLibreApiContext } from \"../mercadolibre-api.js\";\nimport { searchMercadoLibreProductAdsCampaigns } from \"../mercadolibre-product-ads.js\";\n\nvi.mock(\"../mercadolibre-api.js\", async () => {\n const actual = await vi.importActual<typeof import(\"../mercadolibre-api.js\")>(\"../mercadolibre-api.js\");\n return { ...actual, getMercadoLibreApiContext: vi.fn() };\n});\n\ndescribe(\"mercadolibre product ads campaigns service\", () => {\n const getMock = vi.fn();\n const params = { siteId: \"MLA\", advertiserId: \"89015\", dateFrom: \"2026-06-01\", dateTo: \"2026-06-30\" };\n\n beforeEach(() => {\n vi.clearAllMocks();\n vi.mocked(getMercadoLibreApiContext).mockResolvedValue({ api: { get: getMock } } as never);\n });\n\n it(\"uses the validated campaigns search path, page query, and api-version 2 header\", async () => {\n const results = [{ id: \"campaign-1\", name: \"Campaign\", budget: { amount: 1000 } }];\n getMock.mockResolvedValueOnce({ data: { paging: { total: 5, limit: 1, offset: 1 }, results } });\n\n await expect(searchMercadoLibreProductAdsCampaigns(\"triplim\", { ...params, limit: 1, offset: 1 })).resolves.toEqual({\n paging: { total: 5, limit: 1, offset: 1 }, results,\n });\n expect(getMock).toHaveBeenCalledWith(\n \"/advertising/MLA/advertisers/89015/product_ads/campaigns/search\",\n {\n params: { date_from: \"2026-06-01\", date_to: \"2026-06-30\", limit: 1, offset: 1 },\n headers: { \"api-version\": \"2\" },\n timeout: 10_000,\n }\n );\n });\n\n it.each([{ paging: {} }, { paging: { total: 5, limit: \"bad\", offset: 0 } }])(\n \"rejects malformed paging instead of fabricating continuation\",\n async ({ paging }) => {\n getMock.mockResolvedValueOnce({ data: { paging, results: [] } });\n await expect(searchMercadoLibreProductAdsCampaigns(\"triplim\", params)).rejects.toThrow(\n \"campaigns response has missing or invalid paging metadata\"\n );\n }\n );\n\n it.each([401, 403, 404])(\"preserves upstream %s errors\", async (status) => {\n getMock.mockRejectedValueOnce(new MercadoLibreApiError(`upstream ${status}`, status));\n await expect(searchMercadoLibreProductAdsCampaigns(\"triplim\", params)).rejects.toMatchObject({ status, message: `upstream ${status}` });\n });\n\n it(\"rejects a zero limit before it can create a false terminal page\", async () => {\n await expect(searchMercadoLibreProductAdsCampaigns(\"triplim\", { ...params, limit: 0 })).rejects.toThrow(\n \"limit must be an integer between 1 and 100\"\n );\n expect(getMock).not.toHaveBeenCalled();\n });\n\n it.each([{}, { results: undefined }, { results: {} }, { results: \"not-an-array\" }])(\n \"rejects missing or malformed results instead of treating it as an empty page\",\n async (payload) => {\n getMock.mockResolvedValueOnce({ data: { paging: { total: 0, limit: 1, offset: 0 }, ...payload } });\n await expect(searchMercadoLibreProductAdsCampaigns(\"triplim\", params)).rejects.toThrow(\n \"campaigns response has missing or invalid results\"\n );\n }\n );\n\n it(\"rejects underfilled pages so continuation can never skip undiscovered campaigns\", async () => {\n const onObservation = vi.fn();\n getMock.mockResolvedValueOnce({\n data: { paging: { total: 5, limit: 2, offset: 0 }, results: [{ id: \"campaign-1\" }] },\n });\n await expect(searchMercadoLibreProductAdsCampaigns(\"triplim\", params, { onObservation })).rejects.toThrow(\n \"underfilled or oversized page\"\n );\n expect(onObservation).toHaveBeenCalledWith(expect.objectContaining({ event: \"payload_integrity_failure\" }));\n });\n\n it(\"stops retrying when the campaign discovery latency budget cannot cover the next backoff\", async () => {\n const onObservation = vi.fn();\n getMock.mockRejectedValueOnce(new MercadoLibreApiError(\"temporarily unavailable\", 503));\n\n await expect(\n searchMercadoLibreProductAdsCampaigns(\"triplim\", params, {\n totalBudgetMs: 1,\n baseRetryDelayMs: 50,\n onObservation,\n })\n ).rejects.toMatchObject({ code: \"campaign_discovery_budget_exhausted\" });\n\n expect(getMock).toHaveBeenCalledTimes(1);\n expect(onObservation).toHaveBeenCalledWith(expect.objectContaining({ event: \"failure\", reason: \"latency_budget_exhausted\" }));\n });\n\n it(\"emits retry and retry-exhaustion observations with a bounded per-attempt timeout\", async () => {\n const onObservation = vi.fn();\n getMock.mockRejectedValue(new MercadoLibreApiError(\"temporarily unavailable\", 503));\n\n await expect(\n searchMercadoLibreProductAdsCampaigns(\"triplim\", params, {\n maxRetries: 1,\n baseRetryDelayMs: 50,\n totalBudgetMs: 1_000,\n requestTimeoutMs: 123,\n onObservation,\n })\n ).rejects.toThrow(\"temporarily unavailable\");\n\n expect(getMock).toHaveBeenCalledTimes(2);\n expect(getMock.mock.calls[0]?.[1]).toEqual(expect.objectContaining({ timeout: 123 }));\n expect(onObservation).toHaveBeenCalledWith(expect.objectContaining({ event: \"retry\", attempt: 1, status: 503 }));\n expect(onObservation).toHaveBeenCalledWith(expect.objectContaining({ event: \"failure\", attempt: 2, reason: \"retry_exhausted\" }));\n });\n});\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,350 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { MercadoLibreApiError } from "../mercadolibre-api.js";
3
+ import {
4
+ listMercadoLibreProductAdsAdvertisers,
5
+ searchMercadoLibreProductAdsAds,
6
+ searchMercadoLibreProductAdsAdsBatch
7
+ } from "../mercadolibre-product-ads.js";
8
+ import { getMercadoLibreApiContext, runMercadoLibreBatch } from "../mercadolibre-api.js";
9
+ vi.mock("../mercadolibre-api.js", async () => {
10
+ const actual = await vi.importActual("../mercadolibre-api.js");
11
+ return {
12
+ ...actual,
13
+ getMercadoLibreApiContext: vi.fn(),
14
+ runMercadoLibreBatch: vi.fn()
15
+ };
16
+ });
17
+ describe("mercadolibre-product-ads service", () => {
18
+ const getMock = vi.fn();
19
+ const searchRequest = {
20
+ dateFrom: "2026-06-01",
21
+ dateTo: "2026-06-30",
22
+ metrics: ["direct_amount", "indirect_amount", "total_amount", "clicks", "impressions", "cost"],
23
+ metricsSummary: true,
24
+ campaignId: "campaign-1",
25
+ adGroupId: "ad-group-1",
26
+ itemId: "item-1",
27
+ channel: "mshops",
28
+ status: "active",
29
+ brandValueName: "Brand 1"
30
+ };
31
+ beforeEach(() => {
32
+ vi.clearAllMocks();
33
+ vi.useRealTimers();
34
+ vi.mocked(getMercadoLibreApiContext).mockResolvedValue({
35
+ api: { get: getMock },
36
+ sellerId: "seller-1",
37
+ accessToken: "token-1"
38
+ });
39
+ });
40
+ it("retries transient Product Ads advertiser discovery failures before succeeding", async () => {
41
+ const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout").mockImplementation(((handler) => {
42
+ if (typeof handler === "function") {
43
+ handler();
44
+ }
45
+ return 0;
46
+ }));
47
+ getMock.mockRejectedValueOnce(new MercadoLibreApiError("rate limited", 429)).mockRejectedValueOnce(new MercadoLibreApiError("rate limited", 429)).mockResolvedValueOnce({
48
+ data: {
49
+ advertisers: [
50
+ {
51
+ advertiser_id: "adv-1",
52
+ advertiser_site_id: "MLA"
53
+ }
54
+ ]
55
+ }
56
+ });
57
+ const promise = listMercadoLibreProductAdsAdvertisers("profile-1");
58
+ await expect(promise).resolves.toEqual([
59
+ {
60
+ advertiser_id: "adv-1",
61
+ advertiser_site_id: "MLA",
62
+ account_name: "",
63
+ business_name: "",
64
+ status: void 0
65
+ }
66
+ ]);
67
+ expect(getMock).toHaveBeenCalledTimes(3);
68
+ setTimeoutSpy.mockRestore();
69
+ });
70
+ it("lists Product Ads advertisers with the expected discovery endpoint", async () => {
71
+ getMock.mockResolvedValueOnce({
72
+ data: {
73
+ advertisers: [
74
+ {
75
+ advertiser_id: "adv-1",
76
+ advertiser_site_id: "MLA",
77
+ account_name: "Main account",
78
+ business_name: "Main business",
79
+ status: "active"
80
+ }
81
+ ]
82
+ }
83
+ });
84
+ await expect(listMercadoLibreProductAdsAdvertisers("profile-1")).resolves.toEqual([
85
+ {
86
+ advertiser_id: "adv-1",
87
+ advertiser_site_id: "MLA",
88
+ account_name: "Main account",
89
+ business_name: "Main business",
90
+ status: "active"
91
+ }
92
+ ]);
93
+ expect(getMock).toHaveBeenCalledWith("/advertising/advertisers", {
94
+ params: { product_id: "PADS" },
95
+ headers: { "Api-Version": "v1" }
96
+ });
97
+ });
98
+ it("searches Product Ads ads with the expected site and advertiser path", async () => {
99
+ getMock.mockResolvedValueOnce({
100
+ data: {
101
+ paging: { total: 1, limit: 50, offset: 0 },
102
+ results: [
103
+ {
104
+ item_id: "item-1",
105
+ title: "Item one",
106
+ status: "active",
107
+ campaign_id: "campaign-1",
108
+ campaign_name: "Campaign one",
109
+ ad_group_id: "ad-group-1",
110
+ ad_group_name: "Ad group one",
111
+ channel: "mshops",
112
+ brand_value_name: "Brand one",
113
+ metrics: {
114
+ direct_amount: 12.5,
115
+ indirect_amount: 2.5,
116
+ total_amount: 15,
117
+ clicks: 7,
118
+ impressions: 120,
119
+ cost: 9.75,
120
+ advertising_items_quantity: 1,
121
+ currency_id: "ARS"
122
+ }
123
+ }
124
+ ]
125
+ }
126
+ });
127
+ await expect(
128
+ searchMercadoLibreProductAdsAds("profile-1", {
129
+ siteId: "MLA",
130
+ advertiserId: "89015",
131
+ dateFrom: searchRequest.dateFrom,
132
+ dateTo: searchRequest.dateTo,
133
+ metrics: searchRequest.metrics,
134
+ metricsSummary: searchRequest.metricsSummary,
135
+ campaignId: searchRequest.campaignId,
136
+ adGroupId: searchRequest.adGroupId,
137
+ itemId: searchRequest.itemId,
138
+ channel: searchRequest.channel,
139
+ status: searchRequest.status,
140
+ brandValueName: searchRequest.brandValueName,
141
+ limit: 25,
142
+ offset: 10
143
+ })
144
+ ).resolves.toEqual({
145
+ paging: { total: 1, limit: 50, offset: 0 },
146
+ results: [
147
+ {
148
+ item_id: "item-1",
149
+ title: "Item one",
150
+ status: "active",
151
+ campaign_id: "campaign-1",
152
+ campaign_name: "Campaign one",
153
+ ad_group_id: "ad-group-1",
154
+ ad_group_name: "Ad group one",
155
+ channel: "mshops",
156
+ brand_value_name: "Brand one",
157
+ metrics: {
158
+ direct_amount: 12.5,
159
+ indirect_amount: 2.5,
160
+ total_amount: 15,
161
+ clicks: 7,
162
+ impressions: 120,
163
+ cost: 9.75,
164
+ advertising_items_quantity: 1,
165
+ currency_id: "ARS"
166
+ }
167
+ }
168
+ ]
169
+ });
170
+ expect(getMock).toHaveBeenCalledWith(
171
+ "/advertising/MLA/advertisers/89015/product_ads/ads/search",
172
+ {
173
+ params: {
174
+ date_from: searchRequest.dateFrom,
175
+ date_to: searchRequest.dateTo,
176
+ metrics: searchRequest.metrics.join(","),
177
+ metrics_summary: searchRequest.metricsSummary,
178
+ campaign_id: searchRequest.campaignId,
179
+ ad_group_id: searchRequest.adGroupId,
180
+ item_id: searchRequest.itemId,
181
+ channel: searchRequest.channel,
182
+ status: searchRequest.status,
183
+ brand_value_name: searchRequest.brandValueName,
184
+ limit: 25,
185
+ offset: 10
186
+ },
187
+ headers: { "Api-Version": "v2" }
188
+ }
189
+ );
190
+ });
191
+ it("returns no fabricated ads rows when the search envelope has only paging metadata", async () => {
192
+ getMock.mockResolvedValueOnce({
193
+ data: {
194
+ paging: { total: 0, limit: 50, offset: 0 }
195
+ }
196
+ });
197
+ await expect(
198
+ searchMercadoLibreProductAdsAds("profile-1", {
199
+ siteId: "MLA",
200
+ advertiserId: "89015",
201
+ dateFrom: searchRequest.dateFrom,
202
+ dateTo: searchRequest.dateTo,
203
+ metrics: searchRequest.metrics,
204
+ limit: 25,
205
+ offset: 10
206
+ })
207
+ ).resolves.toEqual({
208
+ paging: { total: 0, limit: 50, offset: 0 },
209
+ results: []
210
+ });
211
+ });
212
+ it("exhausts retry attempts with a clear failure when Product Ads search keeps returning transient errors", async () => {
213
+ const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout").mockImplementation(((handler) => {
214
+ if (typeof handler === "function") {
215
+ handler();
216
+ }
217
+ return 0;
218
+ }));
219
+ getMock.mockRejectedValue(new MercadoLibreApiError("temporarily unavailable", 503));
220
+ const promise = searchMercadoLibreProductAdsAds("profile-1", {
221
+ siteId: "MLA",
222
+ advertiserId: "89015",
223
+ dateFrom: searchRequest.dateFrom,
224
+ dateTo: searchRequest.dateTo,
225
+ metrics: searchRequest.metrics,
226
+ limit: 25,
227
+ offset: 10
228
+ });
229
+ await expect(promise).rejects.toMatchObject({
230
+ name: "MercadoLibreApiError",
231
+ status: 503,
232
+ message: "temporarily unavailable"
233
+ });
234
+ expect(getMock).toHaveBeenCalledTimes(3);
235
+ setTimeoutSpy.mockRestore();
236
+ });
237
+ it("delegates Product Ads page batches to the shared batch runner", async () => {
238
+ vi.mocked(runMercadoLibreBatch).mockImplementation(async (ids, fetcher) => ({
239
+ successful: [{ id: ids[0], document: await fetcher(ids[0]) }],
240
+ failed: []
241
+ }));
242
+ getMock.mockResolvedValueOnce({
243
+ data: {
244
+ paging: { total: 2, limit: 50, offset: 50 },
245
+ results: [
246
+ {
247
+ item_id: "item-2",
248
+ title: "Item two",
249
+ status: "paused",
250
+ campaign_id: "campaign-2",
251
+ campaign_name: "Campaign two",
252
+ ad_group_id: "ad-group-2",
253
+ ad_group_name: "Ad group two",
254
+ channel: "marketplace",
255
+ brand_value_name: "Brand two",
256
+ metrics: {
257
+ direct_amount: 25,
258
+ indirect_amount: 5,
259
+ total_amount: 30,
260
+ clicks: 14,
261
+ impressions: 240,
262
+ cost: 19.5,
263
+ advertising_items_quantity: 1,
264
+ currency_id: "ARS"
265
+ }
266
+ }
267
+ ]
268
+ }
269
+ });
270
+ await expect(
271
+ searchMercadoLibreProductAdsAdsBatch(
272
+ "profile-1",
273
+ {
274
+ siteId: "MLA",
275
+ advertiserId: "89015",
276
+ dateFrom: searchRequest.dateFrom,
277
+ dateTo: searchRequest.dateTo,
278
+ metrics: searchRequest.metrics,
279
+ metricsSummary: searchRequest.metricsSummary,
280
+ campaignId: searchRequest.campaignId,
281
+ adGroupId: searchRequest.adGroupId,
282
+ itemId: searchRequest.itemId,
283
+ channel: searchRequest.channel,
284
+ status: searchRequest.status,
285
+ brandValueName: searchRequest.brandValueName,
286
+ limit: 50
287
+ },
288
+ [50],
289
+ { maxConcurrency: 3 }
290
+ )
291
+ ).resolves.toEqual({
292
+ successful: [
293
+ {
294
+ id: "50",
295
+ document: {
296
+ paging: { total: 2, limit: 50, offset: 50 },
297
+ results: [
298
+ {
299
+ item_id: "item-2",
300
+ title: "Item two",
301
+ status: "paused",
302
+ campaign_id: "campaign-2",
303
+ campaign_name: "Campaign two",
304
+ ad_group_id: "ad-group-2",
305
+ ad_group_name: "Ad group two",
306
+ channel: "marketplace",
307
+ brand_value_name: "Brand two",
308
+ metrics: {
309
+ direct_amount: 25,
310
+ indirect_amount: 5,
311
+ total_amount: 30,
312
+ clicks: 14,
313
+ impressions: 240,
314
+ cost: 19.5,
315
+ advertising_items_quantity: 1,
316
+ currency_id: "ARS"
317
+ }
318
+ }
319
+ ]
320
+ }
321
+ }
322
+ ],
323
+ failed: []
324
+ });
325
+ expect(runMercadoLibreBatch).toHaveBeenCalledWith(["50"], expect.any(Function), {
326
+ maxConcurrency: 3
327
+ });
328
+ expect(getMock).toHaveBeenCalledWith(
329
+ "/advertising/MLA/advertisers/89015/product_ads/ads/search",
330
+ {
331
+ params: {
332
+ date_from: searchRequest.dateFrom,
333
+ date_to: searchRequest.dateTo,
334
+ metrics: searchRequest.metrics.join(","),
335
+ metrics_summary: searchRequest.metricsSummary,
336
+ campaign_id: searchRequest.campaignId,
337
+ ad_group_id: searchRequest.adGroupId,
338
+ item_id: searchRequest.itemId,
339
+ channel: searchRequest.channel,
340
+ status: searchRequest.status,
341
+ brand_value_name: searchRequest.brandValueName,
342
+ limit: 50,
343
+ offset: 50
344
+ },
345
+ headers: { "Api-Version": "v2" }
346
+ }
347
+ );
348
+ });
349
+ });
350
+ //# sourceMappingURL=mercadolibre-product-ads.test.js.map