@meuecommerce/frete-adapter-node 0.2.0 → 0.3.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.
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createCorreiosHttpClient = createCorreiosHttpClient;
4
+ /**
5
+ * Node implementation of the `CorreiosClient` port using global `fetch`.
6
+ *
7
+ * Ports the retry/backoff behavior from the Velo backend:
8
+ * - `auth-methods.js` -> `authenticate` (up to 5 tries, fail fast on 4xx)
9
+ * - `estimate-methods.js` -> `getPrice`/`getTime` (timeout + 2 retries, backoff)
10
+ *
11
+ * The only host dependency is `fetch`, injectable for tests.
12
+ */
13
+ const frete_1 = require("@meuecommerce/frete");
14
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
15
+ function base64(input) {
16
+ return Buffer.from(input, "utf-8").toString("base64");
17
+ }
18
+ function createCorreiosHttpClient(options = {}) {
19
+ const { fetch = globalThis.fetch, baseUrl = frete_1.correiosApiUrl, timeoutMs = 20000, estimateRetries = 2, estimateRetryDelayMs = 500, authRetries = 5, authRetryDelayMs = 1000, logger = console, } = options;
20
+ if (typeof fetch !== "function") {
21
+ throw new Error("createCorreiosHttpClient: no fetch available (Node 18+ or pass options.fetch)");
22
+ }
23
+ /** Run a fetch with a hard timeout + exponential-backoff retry. */
24
+ async function fetchWithRetry(url, init) {
25
+ let lastError;
26
+ for (let attempt = 0; attempt < estimateRetries; attempt++) {
27
+ const controller = new AbortController();
28
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
29
+ try {
30
+ return await fetch(url, { ...init, signal: controller.signal });
31
+ }
32
+ catch (error) {
33
+ lastError = error;
34
+ if (attempt === estimateRetries - 1)
35
+ break;
36
+ await sleep(estimateRetryDelayMs * Math.pow(2, attempt));
37
+ }
38
+ finally {
39
+ clearTimeout(timer);
40
+ }
41
+ }
42
+ throw lastError;
43
+ }
44
+ async function estimate(path, payload, token, tag) {
45
+ const url = `${baseUrl}${path}`;
46
+ const init = {
47
+ method: "POST",
48
+ headers: {
49
+ "Content-Type": "application/json",
50
+ Accept: "application/json",
51
+ Authorization: `Bearer ${token}`,
52
+ },
53
+ body: JSON.stringify(payload),
54
+ };
55
+ const response = await fetchWithRetry(url, init);
56
+ if (response.status === 401) {
57
+ throw { status: 401, message: "unauthorized" };
58
+ }
59
+ if (response.status !== 200 && response.status !== 206) {
60
+ // Correios returns a structured error body on bad requests; the domain
61
+ // layer filters these out by `txErro`, so pass it through unchanged.
62
+ const json = await response.json().catch(() => ({}));
63
+ logger.warn(`[${tag}] invalid response. status=${response.status} body=${JSON.stringify(json)}`);
64
+ return json;
65
+ }
66
+ return (await response.json());
67
+ }
68
+ return {
69
+ async authenticate(credentials) {
70
+ const url = `${baseUrl}/token/v1/autentica/cartaopostagem`;
71
+ let lastError;
72
+ for (let attempt = 0; attempt < authRetries; attempt++) {
73
+ try {
74
+ const response = await fetch(url, {
75
+ method: "POST",
76
+ headers: {
77
+ "Content-Type": "application/json",
78
+ accept: "application/json",
79
+ Authorization: `Basic ${base64(`${credentials.user}:${credentials.apiKey}`)}`,
80
+ },
81
+ body: JSON.stringify({ numero: credentials.postcard }),
82
+ });
83
+ // 4xx come from bad credentials/contract, not transient failures —
84
+ // fail fast instead of burning retries (and seconds) on them.
85
+ if (response.status >= 400 && response.status < 500) {
86
+ throw {
87
+ status: response.status,
88
+ message: response.status === 401
89
+ ? "Erro de autenticação nos correios."
90
+ : "Não foi possível autenticar o contrato nos Correios.",
91
+ };
92
+ }
93
+ return (await response.json());
94
+ }
95
+ catch (error) {
96
+ const status = error?.status;
97
+ if (typeof status === "number" && status >= 400 && status < 500)
98
+ throw error;
99
+ lastError = error;
100
+ if (attempt < authRetries - 1) {
101
+ await sleep(authRetryDelayMs);
102
+ }
103
+ else {
104
+ // apiKey intentionally omitted from logs — it is a secret.
105
+ logger.error("[correios.authenticate]:", error, credentials.user, credentials.postcard);
106
+ throw error;
107
+ }
108
+ }
109
+ }
110
+ throw lastError;
111
+ },
112
+ getPrice(payload, token) {
113
+ return estimate("/preco/v1/nacional", payload, token, "correios.getPrice");
114
+ },
115
+ getTime(payload, token) {
116
+ return estimate("/prazo/v1/nacional", payload, token, "correios.getTime");
117
+ },
118
+ };
119
+ }
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EnvSecretStore = void 0;
4
+ class EnvSecretStore {
5
+ env;
6
+ prefix;
7
+ constructor(options = {}) {
8
+ this.env = options.env ?? process.env;
9
+ this.prefix = options.prefix ?? "MEUFRETE_";
10
+ }
11
+ key(name) {
12
+ return this.prefix + name.replace(/[^a-zA-Z0-9]+/g, "_").toUpperCase();
13
+ }
14
+ async getSecret(name) {
15
+ const value = this.env[this.key(name)];
16
+ return value == null || value === "" ? null : value;
17
+ }
18
+ }
19
+ exports.EnvSecretStore = EnvSecretStore;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InMemorySettingsStore = void 0;
4
+ class InMemorySettingsStore {
5
+ defaults;
6
+ merchants = new Map();
7
+ constructor(options) {
8
+ this.defaults = options.defaults;
9
+ for (const [id, settings] of Object.entries(options.merchants ?? {})) {
10
+ this.merchants.set(id, settings);
11
+ }
12
+ }
13
+ /** Add or replace a merchant's settings. */
14
+ set(instanceId, settings) {
15
+ this.merchants.set(instanceId, settings);
16
+ }
17
+ async getMerchantSettings(instanceId) {
18
+ return this.merchants.get(instanceId) ?? null;
19
+ }
20
+ async getDefaultSettings() {
21
+ return this.defaults;
22
+ }
23
+ }
24
+ exports.InMemorySettingsStore = InMemorySettingsStore;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createOpenAIBoxDistributor = exports.createOpenAIDimensionEstimator = exports.EnvSecretStore = exports.InMemorySettingsStore = exports.createCorreiosHttpClient = void 0;
4
+ /**
5
+ * @meuecommerce/frete-adapter-node — Node implementations of @meuecommerce/frete ports.
6
+ *
7
+ * Wraps global `fetch` so the shipping core can run outside Velo: on the
8
+ * Shopify/Fly deployment today, and the MCP server next.
9
+ */
10
+ var correiosHttpClient_js_1 = require("./correiosHttpClient.js");
11
+ Object.defineProperty(exports, "createCorreiosHttpClient", { enumerable: true, get: function () { return correiosHttpClient_js_1.createCorreiosHttpClient; } });
12
+ var inMemorySettingsStore_js_1 = require("./inMemorySettingsStore.js");
13
+ Object.defineProperty(exports, "InMemorySettingsStore", { enumerable: true, get: function () { return inMemorySettingsStore_js_1.InMemorySettingsStore; } });
14
+ var envSecretStore_js_1 = require("./envSecretStore.js");
15
+ Object.defineProperty(exports, "EnvSecretStore", { enumerable: true, get: function () { return envSecretStore_js_1.EnvSecretStore; } });
16
+ var openAIDimensionEstimator_js_1 = require("./openAIDimensionEstimator.js");
17
+ Object.defineProperty(exports, "createOpenAIDimensionEstimator", { enumerable: true, get: function () { return openAIDimensionEstimator_js_1.createOpenAIDimensionEstimator; } });
18
+ var openAIBoxDistributor_js_1 = require("./openAIBoxDistributor.js");
19
+ Object.defineProperty(exports, "createOpenAIBoxDistributor", { enumerable: true, get: function () { return openAIBoxDistributor_js_1.createOpenAIBoxDistributor; } });
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createOpenAIBoxDistributor = createOpenAIBoxDistributor;
4
+ /**
5
+ * `ProductBoxDistributor` implemented with OpenAI — the AI that distributes
6
+ * products into boxes (a second call from the Velo `openAI/box-methods.js`
7
+ * `aiDistributeProducts`, tool `distribute_products`).
8
+ *
9
+ * Best-effort: `packBoxes` validates the output and falls back to the
10
+ * deterministic distribution, so a bad/failed call never breaks packing. `fetch`
11
+ * is injectable (Velo passes `wix-fetch`); the API key is passed in.
12
+ */
13
+ const frete_1 = require("@meuecommerce/frete");
14
+ const DISTRIBUTE_TOOL = {
15
+ type: "function",
16
+ function: {
17
+ name: "distribute_products",
18
+ description: "Distribui TODOS os produtos nas quantidades EXATAS - NUNCA perca produtos ou invente nomes",
19
+ parameters: {
20
+ type: "object",
21
+ properties: {
22
+ boxes: {
23
+ type: "array",
24
+ items: {
25
+ type: "object",
26
+ properties: {
27
+ box: { type: "string", description: "ID EXATO da caixa - NUNCA invente" },
28
+ products: {
29
+ type: "array",
30
+ items: {
31
+ type: "object",
32
+ properties: { name: { type: "string" }, quantity: { type: "number", minimum: 1 } },
33
+ required: ["name", "quantity"],
34
+ },
35
+ },
36
+ dimensions: {
37
+ type: "object",
38
+ properties: { width: { type: "number" }, height: { type: "number" }, length: { type: "number" } },
39
+ required: ["width", "height", "length"],
40
+ },
41
+ },
42
+ required: ["box", "products", "dimensions"],
43
+ },
44
+ },
45
+ },
46
+ required: ["boxes"],
47
+ },
48
+ },
49
+ };
50
+ function findByName(name, items) {
51
+ return items.find((i) => i.name === name);
52
+ }
53
+ function createPrompt(products, boxes, dims) {
54
+ const totalVolume = dims.products.reduce((total, p) => {
55
+ const product = findByName(p.name, products);
56
+ return product ? total + (0, frete_1.calculateVolume)(p.length, p.width, p.height) * product.quantity : total;
57
+ }, 0);
58
+ const boxLines = boxes
59
+ .map((box) => {
60
+ const cap = (0, frete_1.calculateVolume)(box.width, box.height, box.length);
61
+ const pct = Math.round((totalVolume / cap) * 100);
62
+ return `• ${box.id ?? box.name} (${box.name}): ${cap}cm³ — ${pct}% ${pct > 80 ? "❌ REJEITADA" : "✅ APROVADA"}`;
63
+ })
64
+ .join("\n");
65
+ const productLines = dims.products
66
+ .map((p) => {
67
+ const product = findByName(p.name, products);
68
+ if (!product)
69
+ return `• ${p.name}: não encontrado no carrinho`;
70
+ const vol = (0, frete_1.calculateVolume)(p.length, p.width, p.height);
71
+ return `• ${p.name}: ${product.quantity}x — ${p.length}×${p.width}×${p.height}cm — total ${vol * product.quantity}cm³`;
72
+ })
73
+ .join("\n");
74
+ return (`Especialista em logística: distribua TODOS os produtos nas caixas.\n\n` +
75
+ `REGRA: o volume dos produtos numa caixa não pode passar de 80% da capacidade dela.\n` +
76
+ `Volume total dos produtos: ${totalVolume}cm³\n\n` +
77
+ `CAIXAS:\n${boxLines}\n\nPRODUTOS:\n${productLines}\n\n` +
78
+ `PRIORIDADE ABSOLUTA: menor número de caixas = menor custo. Use 1 caixa quando couber; ` +
79
+ `só use múltiplas se for matematicamente impossível. Use IDs de caixa EXATOS e preserve os nomes e quantidades.\n` +
80
+ `Responda usando a função 'distribute_products'.`);
81
+ }
82
+ function createOpenAIBoxDistributor(options) {
83
+ const { apiKey, fetch = globalThis.fetch, model = "gpt-4o-mini", temperature = 0.5, apiUrl = "https://api.openai.com/v1/chat/completions", timeoutMs = 20000, } = options;
84
+ if (!apiKey)
85
+ throw new Error("createOpenAIBoxDistributor: apiKey is required");
86
+ if (typeof fetch !== "function")
87
+ throw new Error("createOpenAIBoxDistributor: no fetch available");
88
+ return {
89
+ async distribute(products, boxes, estimatedDimensions) {
90
+ const body = {
91
+ model,
92
+ messages: [
93
+ { role: "system", content: "Você é um especialista em otimização logística. SEMPRE maximize consolidação e minimize número de caixas." },
94
+ { role: "user", content: createPrompt(products, boxes, estimatedDimensions) },
95
+ ],
96
+ tools: [DISTRIBUTE_TOOL],
97
+ tool_choice: { type: "function", function: { name: "distribute_products" } },
98
+ temperature,
99
+ };
100
+ const controller = new AbortController();
101
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
102
+ let response;
103
+ try {
104
+ response = await fetch(apiUrl, {
105
+ method: "POST",
106
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
107
+ body: JSON.stringify(body),
108
+ signal: controller.signal,
109
+ });
110
+ }
111
+ finally {
112
+ clearTimeout(timer);
113
+ }
114
+ if (!response.ok)
115
+ throw new Error(`OpenAI API Error: ${response.status} ${response.statusText}`);
116
+ const json = (await response.json());
117
+ if (json.error)
118
+ throw new Error(`OpenAI Error: ${json.error.message}`);
119
+ const args = json.choices?.[0]?.message?.tool_calls?.[0]?.function?.arguments;
120
+ if (!args)
121
+ throw new Error("IA não conseguiu distribuir os produtos");
122
+ return JSON.parse(args);
123
+ },
124
+ };
125
+ }
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createOpenAIDimensionEstimator = createOpenAIDimensionEstimator;
4
+ const ESTIMATE_DIMENSIONS_TOOL = {
5
+ type: "function",
6
+ function: {
7
+ name: "estimate_dimensions",
8
+ description: "Estima dimensões de produtos PRESERVANDO os nomes exatos fornecidos",
9
+ parameters: {
10
+ type: "object",
11
+ properties: {
12
+ products: {
13
+ type: "array",
14
+ items: {
15
+ type: "object",
16
+ properties: {
17
+ name: { type: "string" },
18
+ length: { type: "number", description: "Comprimento em cm" },
19
+ width: { type: "number", description: "Largura em cm" },
20
+ height: { type: "number", description: "Altura em cm" },
21
+ },
22
+ required: ["name", "length", "width", "height"],
23
+ },
24
+ },
25
+ },
26
+ required: ["products"],
27
+ },
28
+ },
29
+ };
30
+ function largestBox(boxes) {
31
+ return boxes.reduce((largest, current) => current.width * current.height * current.length > largest.width * largest.height * largest.length ? current : largest);
32
+ }
33
+ function createPrompt(products, boxes, samples) {
34
+ const box = largestBox(boxes);
35
+ const maxWidth = Math.floor(box.width * 0.8);
36
+ const maxHeight = Math.floor(box.height * 0.8);
37
+ const maxLength = Math.floor(box.length * 0.8);
38
+ const samplesSection = samples.length > 0
39
+ ? `\n🌟 EXEMPLOS DO USUÁRIO (PRIORIDADE MÁXIMA):\n${samples.map((s, i) => `${i + 1}. ${s}`).join("\n")}\n\n` +
40
+ `📋 INSTRUÇÕES:\n1. Para cada produto, procure um exemplo similar acima\n2. Match exato → use dimensões EXATAS\n3. Similar → ajuste\n4. Sem match → conhecimento geral\n`
41
+ : `\n📋 INSTRUÇÕES (SEM EXEMPLOS):\n1. Considere a embalagem comercial típica brasileira\n2. Use conhecimento sobre produtos similares\n3. Ajuste baseado no peso\n`;
42
+ return (`Você é um especialista em dimensões de produtos brasileiros para e-commerce.\n\n` +
43
+ `🚨 LIMITES FÍSICOS OBRIGATÓRIOS:\n- Largura máxima: ${maxWidth}cm\n- Altura máxima: ${maxHeight}cm\n- Comprimento máximo: ${maxLength}cm\n(Maior caixa: ${box.name ?? "—"})\n\n` +
44
+ `📦 PRODUTOS PARA ESTIMAR:\n${products.map((p, i) => `${i + 1}. ${p.name} - ${p.quantity}x (${p.weight}kg cada)`).join("\n")}\n` +
45
+ samplesSection +
46
+ `\n⚠️ REGRAS: respeite os limites; preserve os nomes EXATOS; retorne length×width×height em cm.\n` +
47
+ `Responda usando a função 'estimate_dimensions'.`);
48
+ }
49
+ /** Apply the largest-box physical limits + a minimum-volume floor to AI output. */
50
+ function validateAndOptimize(aiResult, products, boxes) {
51
+ const box = largestBox(boxes);
52
+ const maxWidth = Math.floor(box.width * 0.8);
53
+ const maxHeight = Math.floor(box.height * 0.8);
54
+ const maxLength = Math.floor(box.length * 0.8);
55
+ const optimized = aiResult.products.map((product) => {
56
+ let { length, width, height } = product;
57
+ if (length > maxLength)
58
+ length = maxLength;
59
+ if (width > maxWidth)
60
+ width = maxWidth;
61
+ if (height > maxHeight)
62
+ height = maxHeight;
63
+ const volume = length * width * height;
64
+ const original = products.find((p) => p.name === product.name);
65
+ const minVolume = (original?.weight || 0.1) * 500;
66
+ if (volume > 0 && volume < minVolume) {
67
+ const scale = Math.cbrt(minVolume / volume);
68
+ length = Math.min(maxLength, length * scale);
69
+ width = Math.min(maxWidth, width * scale);
70
+ height = Math.min(maxHeight, height * scale);
71
+ }
72
+ return {
73
+ name: product.name,
74
+ length: Math.round(length * 10) / 10,
75
+ width: Math.round(width * 10) / 10,
76
+ height: Math.round(height * 10) / 10,
77
+ };
78
+ });
79
+ return { products: optimized };
80
+ }
81
+ function createOpenAIDimensionEstimator(options) {
82
+ const { apiKey, fetch = globalThis.fetch, model = "gpt-4o-mini", temperature = 0.1, apiUrl = "https://api.openai.com/v1/chat/completions", timeoutMs = 20000, } = options;
83
+ if (!apiKey)
84
+ throw new Error("createOpenAIDimensionEstimator: apiKey is required");
85
+ if (typeof fetch !== "function")
86
+ throw new Error("createOpenAIDimensionEstimator: no fetch available");
87
+ return {
88
+ async estimate(products, boxes, productSamples = []) {
89
+ if (!boxes.length)
90
+ throw new Error("estimate: no boxes provided");
91
+ const body = {
92
+ model,
93
+ messages: [
94
+ {
95
+ role: "system",
96
+ content: "Você é um especialista em dimensões de produtos. Use os exemplos personalizados como prioridade máxima quando o nome bater.",
97
+ },
98
+ { role: "user", content: createPrompt(products, boxes, productSamples) },
99
+ ],
100
+ tools: [ESTIMATE_DIMENSIONS_TOOL],
101
+ tool_choice: { type: "function", function: { name: "estimate_dimensions" } },
102
+ temperature,
103
+ };
104
+ const controller = new AbortController();
105
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
106
+ let response;
107
+ try {
108
+ response = await fetch(apiUrl, {
109
+ method: "POST",
110
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
111
+ body: JSON.stringify(body),
112
+ signal: controller.signal,
113
+ });
114
+ }
115
+ finally {
116
+ clearTimeout(timer);
117
+ }
118
+ if (!response.ok)
119
+ throw new Error(`OpenAI API Error: ${response.status} ${response.statusText}`);
120
+ const json = (await response.json());
121
+ if (json.error)
122
+ throw new Error(`OpenAI Error: ${json.error.message}`);
123
+ const args = json.choices?.[0]?.message?.tool_calls?.[0]?.function?.arguments;
124
+ if (!args)
125
+ throw new Error("IA não conseguiu estimar dimensões dos produtos");
126
+ const parsed = JSON.parse(args);
127
+ return validateAndOptimize(parsed, products, boxes);
128
+ },
129
+ };
130
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
package/dist/index.d.ts CHANGED
@@ -12,3 +12,5 @@ export { EnvSecretStore } from "./envSecretStore.js";
12
12
  export type { EnvSecretStoreOptions } from "./envSecretStore.js";
13
13
  export { createOpenAIDimensionEstimator } from "./openAIDimensionEstimator.js";
14
14
  export type { OpenAIDimensionEstimatorOptions } from "./openAIDimensionEstimator.js";
15
+ export { createOpenAIBoxDistributor } from "./openAIBoxDistributor.js";
16
+ export type { OpenAIBoxDistributorOptions } from "./openAIBoxDistributor.js";
package/dist/index.js CHANGED
@@ -8,3 +8,4 @@ export { createCorreiosHttpClient } from "./correiosHttpClient.js";
8
8
  export { InMemorySettingsStore } from "./inMemorySettingsStore.js";
9
9
  export { EnvSecretStore } from "./envSecretStore.js";
10
10
  export { createOpenAIDimensionEstimator } from "./openAIDimensionEstimator.js";
11
+ export { createOpenAIBoxDistributor } from "./openAIBoxDistributor.js";
@@ -0,0 +1,21 @@
1
+ /**
2
+ * `ProductBoxDistributor` implemented with OpenAI — the AI that distributes
3
+ * products into boxes (a second call from the Velo `openAI/box-methods.js`
4
+ * `aiDistributeProducts`, tool `distribute_products`).
5
+ *
6
+ * Best-effort: `packBoxes` validates the output and falls back to the
7
+ * deterministic distribution, so a bad/failed call never breaks packing. `fetch`
8
+ * is injectable (Velo passes `wix-fetch`); the API key is passed in.
9
+ */
10
+ import { type ProductBoxDistributor } from "@meuecommerce/frete";
11
+ type FetchLike = typeof globalThis.fetch;
12
+ export interface OpenAIBoxDistributorOptions {
13
+ apiKey: string;
14
+ fetch?: FetchLike;
15
+ model?: string;
16
+ temperature?: number;
17
+ apiUrl?: string;
18
+ timeoutMs?: number;
19
+ }
20
+ export declare function createOpenAIBoxDistributor(options: OpenAIBoxDistributorOptions): ProductBoxDistributor;
21
+ export {};
@@ -0,0 +1,122 @@
1
+ /**
2
+ * `ProductBoxDistributor` implemented with OpenAI — the AI that distributes
3
+ * products into boxes (a second call from the Velo `openAI/box-methods.js`
4
+ * `aiDistributeProducts`, tool `distribute_products`).
5
+ *
6
+ * Best-effort: `packBoxes` validates the output and falls back to the
7
+ * deterministic distribution, so a bad/failed call never breaks packing. `fetch`
8
+ * is injectable (Velo passes `wix-fetch`); the API key is passed in.
9
+ */
10
+ import { calculateVolume, } from "@meuecommerce/frete";
11
+ const DISTRIBUTE_TOOL = {
12
+ type: "function",
13
+ function: {
14
+ name: "distribute_products",
15
+ description: "Distribui TODOS os produtos nas quantidades EXATAS - NUNCA perca produtos ou invente nomes",
16
+ parameters: {
17
+ type: "object",
18
+ properties: {
19
+ boxes: {
20
+ type: "array",
21
+ items: {
22
+ type: "object",
23
+ properties: {
24
+ box: { type: "string", description: "ID EXATO da caixa - NUNCA invente" },
25
+ products: {
26
+ type: "array",
27
+ items: {
28
+ type: "object",
29
+ properties: { name: { type: "string" }, quantity: { type: "number", minimum: 1 } },
30
+ required: ["name", "quantity"],
31
+ },
32
+ },
33
+ dimensions: {
34
+ type: "object",
35
+ properties: { width: { type: "number" }, height: { type: "number" }, length: { type: "number" } },
36
+ required: ["width", "height", "length"],
37
+ },
38
+ },
39
+ required: ["box", "products", "dimensions"],
40
+ },
41
+ },
42
+ },
43
+ required: ["boxes"],
44
+ },
45
+ },
46
+ };
47
+ function findByName(name, items) {
48
+ return items.find((i) => i.name === name);
49
+ }
50
+ function createPrompt(products, boxes, dims) {
51
+ const totalVolume = dims.products.reduce((total, p) => {
52
+ const product = findByName(p.name, products);
53
+ return product ? total + calculateVolume(p.length, p.width, p.height) * product.quantity : total;
54
+ }, 0);
55
+ const boxLines = boxes
56
+ .map((box) => {
57
+ const cap = calculateVolume(box.width, box.height, box.length);
58
+ const pct = Math.round((totalVolume / cap) * 100);
59
+ return `• ${box.id ?? box.name} (${box.name}): ${cap}cm³ — ${pct}% ${pct > 80 ? "❌ REJEITADA" : "✅ APROVADA"}`;
60
+ })
61
+ .join("\n");
62
+ const productLines = dims.products
63
+ .map((p) => {
64
+ const product = findByName(p.name, products);
65
+ if (!product)
66
+ return `• ${p.name}: não encontrado no carrinho`;
67
+ const vol = calculateVolume(p.length, p.width, p.height);
68
+ return `• ${p.name}: ${product.quantity}x — ${p.length}×${p.width}×${p.height}cm — total ${vol * product.quantity}cm³`;
69
+ })
70
+ .join("\n");
71
+ return (`Especialista em logística: distribua TODOS os produtos nas caixas.\n\n` +
72
+ `REGRA: o volume dos produtos numa caixa não pode passar de 80% da capacidade dela.\n` +
73
+ `Volume total dos produtos: ${totalVolume}cm³\n\n` +
74
+ `CAIXAS:\n${boxLines}\n\nPRODUTOS:\n${productLines}\n\n` +
75
+ `PRIORIDADE ABSOLUTA: menor número de caixas = menor custo. Use 1 caixa quando couber; ` +
76
+ `só use múltiplas se for matematicamente impossível. Use IDs de caixa EXATOS e preserve os nomes e quantidades.\n` +
77
+ `Responda usando a função 'distribute_products'.`);
78
+ }
79
+ export function createOpenAIBoxDistributor(options) {
80
+ const { apiKey, fetch = globalThis.fetch, model = "gpt-4o-mini", temperature = 0.5, apiUrl = "https://api.openai.com/v1/chat/completions", timeoutMs = 20000, } = options;
81
+ if (!apiKey)
82
+ throw new Error("createOpenAIBoxDistributor: apiKey is required");
83
+ if (typeof fetch !== "function")
84
+ throw new Error("createOpenAIBoxDistributor: no fetch available");
85
+ return {
86
+ async distribute(products, boxes, estimatedDimensions) {
87
+ const body = {
88
+ model,
89
+ messages: [
90
+ { role: "system", content: "Você é um especialista em otimização logística. SEMPRE maximize consolidação e minimize número de caixas." },
91
+ { role: "user", content: createPrompt(products, boxes, estimatedDimensions) },
92
+ ],
93
+ tools: [DISTRIBUTE_TOOL],
94
+ tool_choice: { type: "function", function: { name: "distribute_products" } },
95
+ temperature,
96
+ };
97
+ const controller = new AbortController();
98
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
99
+ let response;
100
+ try {
101
+ response = await fetch(apiUrl, {
102
+ method: "POST",
103
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
104
+ body: JSON.stringify(body),
105
+ signal: controller.signal,
106
+ });
107
+ }
108
+ finally {
109
+ clearTimeout(timer);
110
+ }
111
+ if (!response.ok)
112
+ throw new Error(`OpenAI API Error: ${response.status} ${response.statusText}`);
113
+ const json = (await response.json());
114
+ if (json.error)
115
+ throw new Error(`OpenAI Error: ${json.error.message}`);
116
+ const args = json.choices?.[0]?.message?.tool_calls?.[0]?.function?.arguments;
117
+ if (!args)
118
+ throw new Error("IA não conseguiu distribuir os produtos");
119
+ return JSON.parse(args);
120
+ },
121
+ };
122
+ }
package/package.json CHANGED
@@ -1,14 +1,16 @@
1
1
  {
2
2
  "name": "@meuecommerce/frete-adapter-node",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Node implementation of @meuecommerce/frete ports (Correios HTTP client) using global fetch. Used by the Shopify/Fly deployment and the MCP server.",
5
5
  "type": "module",
6
- "main": "./dist/index.js",
6
+ "main": "./dist/cjs/index.js",
7
7
  "types": "./dist/index.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
10
  "types": "./dist/index.d.ts",
11
- "import": "./dist/index.js"
11
+ "import": "./dist/index.js",
12
+ "require": "./dist/cjs/index.js",
13
+ "default": "./dist/cjs/index.js"
12
14
  }
13
15
  },
14
16
  "files": [
@@ -18,7 +20,7 @@
18
20
  "node": ">=18.18"
19
21
  },
20
22
  "scripts": {
21
- "build": "tsc -p tsconfig.json",
23
+ "build": "tsc -p tsconfig.json && tsc -p tsconfig.cjs.json && node ../../scripts/cjs-pkg.cjs",
22
24
  "prepublishOnly": "npm run build",
23
25
  "typecheck": "tsc --noEmit",
24
26
  "test": "vitest run",
@@ -29,6 +31,7 @@
29
31
  "access": "public"
30
32
  },
31
33
  "dependencies": {
32
- "@meuecommerce/frete": "^0.2.0"
33
- }
34
+ "@meuecommerce/frete": "^0.3.0"
35
+ },
36
+ "module": "./dist/index.js"
34
37
  }