@meuecommerce/frete-adapter-node 0.2.1 → 0.3.1

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,115 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createCorreiosLabelHttpClient = createCorreiosLabelHttpClient;
4
+ /**
5
+ * Node implementation of the `CorreiosLabelClient` port using global `fetch`.
6
+ *
7
+ * Mirrors `correiosHttpClient.ts`: same base URL, per-request timeout, and
8
+ * bounded retry on transient network failures. It stays deliberately thin —
9
+ * one round-trip per method; the "request PDF then poll" loop lives in the
10
+ * `generateLabel` use case.
11
+ *
12
+ * Endpoints (Correios pré-postagem API):
13
+ * - `POST /prepostagem/v1/prepostagens` — create
14
+ * - `POST /prepostagem/v1/prepostagens/rotulo/assincrono/pdf` — request async PDF
15
+ * - `GET /prepostagem/v1/prepostagens/rotulo/download/assincrono/{idRecibo}` — poll/download
16
+ *
17
+ * NOTE: the async download path isn't in the public docs mirror; the default
18
+ * below is the officially-referenced one and is overridable via `downloadPath`
19
+ * so it can be corrected against homolog without touching the use case.
20
+ */
21
+ const frete_1 = require("@meuecommerce/frete");
22
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
23
+ function createCorreiosLabelHttpClient(options = {}) {
24
+ const { fetch = globalThis.fetch, baseUrl = frete_1.correiosApiUrl, timeoutMs = 20000, retries = 2, retryDelayMs = 500, tipoRotulo = "P", downloadPath = "/prepostagem/v1/prepostagens/rotulo/download/assincrono/{idRecibo}", logger = console, } = options;
25
+ if (typeof fetch !== "function") {
26
+ throw new Error("createCorreiosLabelHttpClient: no fetch available (Node 18+ or pass options.fetch)");
27
+ }
28
+ async function fetchWithRetry(url, init) {
29
+ let lastError;
30
+ for (let attempt = 0; attempt < retries; attempt++) {
31
+ const controller = new AbortController();
32
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
33
+ try {
34
+ return await fetch(url, { ...init, signal: controller.signal });
35
+ }
36
+ catch (error) {
37
+ lastError = error;
38
+ if (attempt === retries - 1)
39
+ break;
40
+ await sleep(retryDelayMs * Math.pow(2, attempt));
41
+ }
42
+ finally {
43
+ clearTimeout(timer);
44
+ }
45
+ }
46
+ throw lastError;
47
+ }
48
+ function authHeaders(token) {
49
+ return {
50
+ "Content-Type": "application/json",
51
+ Accept: "application/json",
52
+ Authorization: `Bearer ${token}`,
53
+ };
54
+ }
55
+ return {
56
+ async createPrepostagem(payload, token) {
57
+ const response = await fetchWithRetry(`${baseUrl}/prepostagem/v1/prepostagens`, {
58
+ method: "POST",
59
+ headers: authHeaders(token),
60
+ body: JSON.stringify(payload),
61
+ });
62
+ // Correios returns a structured error body (with the failure reason) on 4xx;
63
+ // the use case filters by `txErro`, so pass it through rather than throwing.
64
+ const json = (await response.json().catch(() => ({})));
65
+ if (response.status !== 200 && response.status !== 201) {
66
+ logger.warn(`[correios.createPrepostagem] status=${response.status} body=${JSON.stringify(json)}`);
67
+ if (!json.txErro)
68
+ json.txErro = `http_${response.status}`;
69
+ }
70
+ return json;
71
+ },
72
+ async requestLabelPdf(ids, token) {
73
+ const response = await fetchWithRetry(`${baseUrl}/prepostagem/v1/prepostagens/rotulo/assincrono/pdf`, {
74
+ method: "POST",
75
+ headers: authHeaders(token),
76
+ body: JSON.stringify({ idsPrePostagem: ids, tipoRotulo }),
77
+ });
78
+ const json = (await response.json().catch(() => ({})));
79
+ if (!json.idRecibo)
80
+ throw new Error(`requestLabelPdf: no idRecibo (status=${response.status})`);
81
+ return { idRecibo: json.idRecibo };
82
+ },
83
+ async downloadLabelPdf(idRecibo, token) {
84
+ const path = downloadPath.replace("{idRecibo}", encodeURIComponent(idRecibo));
85
+ // tipoRotulo is also accepted as a query param on the download (PPN-285).
86
+ const sep = path.includes("?") ? "&" : "?";
87
+ const url = `${baseUrl}${path}${sep}tipoRotulo=${encodeURIComponent(tipoRotulo)}`;
88
+ const response = await fetchWithRetry(url, {
89
+ method: "GET",
90
+ headers: { Accept: "application/pdf, application/json", Authorization: `Bearer ${token}` },
91
+ });
92
+ const contentType = response.headers.get("content-type") ?? "";
93
+ // 202 (or 204) = still processing; tell the use case to poll again.
94
+ if (response.status === 202 || response.status === 204)
95
+ return { ready: false };
96
+ if (response.status !== 200) {
97
+ const body = await response.text().catch(() => "");
98
+ return { ready: false, message: `http_${response.status}: ${body.slice(0, 200)}` };
99
+ }
100
+ // Correios may return the PDF as raw bytes (application/pdf) or wrapped in
101
+ // JSON as base64 (dados/pdf). Handle both.
102
+ if (contentType.includes("application/pdf") || contentType.includes("octet-stream")) {
103
+ const buf = Buffer.from(await response.arrayBuffer());
104
+ return buf.length ? { ready: true, pdfBase64: buf.toString("base64") } : { ready: false };
105
+ }
106
+ const json = (await response.json().catch(() => ({})));
107
+ const pdfBase64 = (json.dados ?? json.pdf ?? json.base64 ?? json.arquivo);
108
+ if (pdfBase64)
109
+ return { ready: true, pdfBase64 };
110
+ // 200 without a PDF carries a business message (e.g. "status Pendente").
111
+ const message = typeof json.mensagem === "string" ? json.mensagem : undefined;
112
+ return message ? { ready: false, message } : { ready: false };
113
+ },
114
+ };
115
+ }
@@ -6,7 +6,7 @@ class EnvSecretStore {
6
6
  prefix;
7
7
  constructor(options = {}) {
8
8
  this.env = options.env ?? process.env;
9
- this.prefix = options.prefix ?? "MEUFRETE_";
9
+ this.prefix = options.prefix ?? "FRETE_";
10
10
  }
11
11
  key(name) {
12
12
  return this.prefix + name.replace(/[^a-zA-Z0-9]+/g, "_").toUpperCase();
package/dist/cjs/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createOpenAIDimensionEstimator = exports.EnvSecretStore = exports.InMemorySettingsStore = exports.createCorreiosHttpClient = void 0;
3
+ exports.createOpenAIBoxDistributor = exports.createOpenAIDimensionEstimator = exports.EnvSecretStore = exports.InMemorySettingsStore = exports.createCorreiosHttpClient = void 0;
4
4
  /**
5
5
  * @meuecommerce/frete-adapter-node — Node implementations of @meuecommerce/frete ports.
6
6
  *
@@ -15,3 +15,5 @@ var envSecretStore_js_1 = require("./envSecretStore.js");
15
15
  Object.defineProperty(exports, "EnvSecretStore", { enumerable: true, get: function () { return envSecretStore_js_1.EnvSecretStore; } });
16
16
  var openAIDimensionEstimator_js_1 = require("./openAIDimensionEstimator.js");
17
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,41 @@
1
+ /**
2
+ * Node implementation of the `CorreiosLabelClient` port using global `fetch`.
3
+ *
4
+ * Mirrors `correiosHttpClient.ts`: same base URL, per-request timeout, and
5
+ * bounded retry on transient network failures. It stays deliberately thin —
6
+ * one round-trip per method; the "request PDF then poll" loop lives in the
7
+ * `generateLabel` use case.
8
+ *
9
+ * Endpoints (Correios pré-postagem API):
10
+ * - `POST /prepostagem/v1/prepostagens` — create
11
+ * - `POST /prepostagem/v1/prepostagens/rotulo/assincrono/pdf` — request async PDF
12
+ * - `GET /prepostagem/v1/prepostagens/rotulo/download/assincrono/{idRecibo}` — poll/download
13
+ *
14
+ * NOTE: the async download path isn't in the public docs mirror; the default
15
+ * below is the officially-referenced one and is overridable via `downloadPath`
16
+ * so it can be corrected against homolog without touching the use case.
17
+ */
18
+ import { type CorreiosLabelClient } from "@meuecommerce/frete";
19
+ type FetchLike = typeof globalThis.fetch;
20
+ export interface CorreiosLabelHttpClientOptions {
21
+ /** Injectable fetch (defaults to global fetch). */
22
+ fetch?: FetchLike;
23
+ /** Correios API base URL (defaults to the production URL). */
24
+ baseUrl?: string;
25
+ /** Per-request timeout, ms. Default 20000. */
26
+ timeoutMs?: number;
27
+ /** Max attempts per call. Default 2. */
28
+ retries?: number;
29
+ /** Base backoff between retries, ms (exponential). Default 500. */
30
+ retryDelayMs?: number;
31
+ /** Label size sent to Correios: "P" padrão or "R" reduzido. Default "P". */
32
+ tipoRotulo?: string;
33
+ /**
34
+ * Download endpoint template; `{idRecibo}` is substituted. Default follows the
35
+ * Correios async-download convention. Override if homolog differs.
36
+ */
37
+ downloadPath?: string;
38
+ logger?: Pick<Console, "warn" | "error" | "log">;
39
+ }
40
+ export declare function createCorreiosLabelHttpClient(options?: CorreiosLabelHttpClientOptions): CorreiosLabelClient;
41
+ export {};
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Node implementation of the `CorreiosLabelClient` port using global `fetch`.
3
+ *
4
+ * Mirrors `correiosHttpClient.ts`: same base URL, per-request timeout, and
5
+ * bounded retry on transient network failures. It stays deliberately thin —
6
+ * one round-trip per method; the "request PDF then poll" loop lives in the
7
+ * `generateLabel` use case.
8
+ *
9
+ * Endpoints (Correios pré-postagem API):
10
+ * - `POST /prepostagem/v1/prepostagens` — create
11
+ * - `POST /prepostagem/v1/prepostagens/rotulo/assincrono/pdf` — request async PDF
12
+ * - `GET /prepostagem/v1/prepostagens/rotulo/download/assincrono/{idRecibo}` — poll/download
13
+ *
14
+ * NOTE: the async download path isn't in the public docs mirror; the default
15
+ * below is the officially-referenced one and is overridable via `downloadPath`
16
+ * so it can be corrected against homolog without touching the use case.
17
+ */
18
+ import { correiosApiUrl, } from "@meuecommerce/frete";
19
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
20
+ export function createCorreiosLabelHttpClient(options = {}) {
21
+ const { fetch = globalThis.fetch, baseUrl = correiosApiUrl, timeoutMs = 20000, retries = 2, retryDelayMs = 500, tipoRotulo = "P", downloadPath = "/prepostagem/v1/prepostagens/rotulo/download/assincrono/{idRecibo}", logger = console, } = options;
22
+ if (typeof fetch !== "function") {
23
+ throw new Error("createCorreiosLabelHttpClient: no fetch available (Node 18+ or pass options.fetch)");
24
+ }
25
+ async function fetchWithRetry(url, init) {
26
+ let lastError;
27
+ for (let attempt = 0; attempt < retries; attempt++) {
28
+ const controller = new AbortController();
29
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
30
+ try {
31
+ return await fetch(url, { ...init, signal: controller.signal });
32
+ }
33
+ catch (error) {
34
+ lastError = error;
35
+ if (attempt === retries - 1)
36
+ break;
37
+ await sleep(retryDelayMs * Math.pow(2, attempt));
38
+ }
39
+ finally {
40
+ clearTimeout(timer);
41
+ }
42
+ }
43
+ throw lastError;
44
+ }
45
+ function authHeaders(token) {
46
+ return {
47
+ "Content-Type": "application/json",
48
+ Accept: "application/json",
49
+ Authorization: `Bearer ${token}`,
50
+ };
51
+ }
52
+ return {
53
+ async createPrepostagem(payload, token) {
54
+ const response = await fetchWithRetry(`${baseUrl}/prepostagem/v1/prepostagens`, {
55
+ method: "POST",
56
+ headers: authHeaders(token),
57
+ body: JSON.stringify(payload),
58
+ });
59
+ // Correios returns a structured error body (with the failure reason) on 4xx;
60
+ // the use case filters by `txErro`, so pass it through rather than throwing.
61
+ const json = (await response.json().catch(() => ({})));
62
+ if (response.status !== 200 && response.status !== 201) {
63
+ logger.warn(`[correios.createPrepostagem] status=${response.status} body=${JSON.stringify(json)}`);
64
+ if (!json.txErro)
65
+ json.txErro = `http_${response.status}`;
66
+ }
67
+ return json;
68
+ },
69
+ async requestLabelPdf(ids, token) {
70
+ const response = await fetchWithRetry(`${baseUrl}/prepostagem/v1/prepostagens/rotulo/assincrono/pdf`, {
71
+ method: "POST",
72
+ headers: authHeaders(token),
73
+ body: JSON.stringify({ idsPrePostagem: ids, tipoRotulo }),
74
+ });
75
+ const json = (await response.json().catch(() => ({})));
76
+ if (!json.idRecibo)
77
+ throw new Error(`requestLabelPdf: no idRecibo (status=${response.status})`);
78
+ return { idRecibo: json.idRecibo };
79
+ },
80
+ async downloadLabelPdf(idRecibo, token) {
81
+ const path = downloadPath.replace("{idRecibo}", encodeURIComponent(idRecibo));
82
+ // tipoRotulo is also accepted as a query param on the download (PPN-285).
83
+ const sep = path.includes("?") ? "&" : "?";
84
+ const url = `${baseUrl}${path}${sep}tipoRotulo=${encodeURIComponent(tipoRotulo)}`;
85
+ const response = await fetchWithRetry(url, {
86
+ method: "GET",
87
+ headers: { Accept: "application/pdf, application/json", Authorization: `Bearer ${token}` },
88
+ });
89
+ const contentType = response.headers.get("content-type") ?? "";
90
+ // 202 (or 204) = still processing; tell the use case to poll again.
91
+ if (response.status === 202 || response.status === 204)
92
+ return { ready: false };
93
+ if (response.status !== 200) {
94
+ const body = await response.text().catch(() => "");
95
+ return { ready: false, message: `http_${response.status}: ${body.slice(0, 200)}` };
96
+ }
97
+ // Correios may return the PDF as raw bytes (application/pdf) or wrapped in
98
+ // JSON as base64 (dados/pdf). Handle both.
99
+ if (contentType.includes("application/pdf") || contentType.includes("octet-stream")) {
100
+ const buf = Buffer.from(await response.arrayBuffer());
101
+ return buf.length ? { ready: true, pdfBase64: buf.toString("base64") } : { ready: false };
102
+ }
103
+ const json = (await response.json().catch(() => ({})));
104
+ const pdfBase64 = (json.dados ?? json.pdf ?? json.base64 ?? json.arquivo);
105
+ if (pdfBase64)
106
+ return { ready: true, pdfBase64 };
107
+ // 200 without a PDF carries a business message (e.g. "status Pendente").
108
+ const message = typeof json.mensagem === "string" ? json.mensagem : undefined;
109
+ return message ? { ready: false, message } : { ready: false };
110
+ },
111
+ };
112
+ }
@@ -4,13 +4,13 @@
4
4
  *
5
5
  * Secret names are normalized to an env-var convention: uppercased with
6
6
  * non-alphanumerics turned into underscores, plus an optional prefix. So
7
- * `getSecret("backfill_secret")` reads `MEUFRETE_BACKFILL_SECRET` by default.
7
+ * `getSecret("backfill_secret")` reads `FRETE_BACKFILL_SECRET` by default.
8
8
  */
9
9
  import type { SecretStore } from "@meuecommerce/frete";
10
10
  export interface EnvSecretStoreOptions {
11
11
  /** Env source (defaults to process.env). */
12
12
  env?: Record<string, string | undefined>;
13
- /** Prefix applied to normalized names. Default "MEUFRETE_". Pass "" for none. */
13
+ /** Prefix applied to normalized names. Default "FRETE_". Pass "" for none. */
14
14
  prefix?: string;
15
15
  }
16
16
  export declare class EnvSecretStore implements SecretStore {
@@ -3,7 +3,7 @@ export class EnvSecretStore {
3
3
  prefix;
4
4
  constructor(options = {}) {
5
5
  this.env = options.env ?? process.env;
6
- this.prefix = options.prefix ?? "MEUFRETE_";
6
+ this.prefix = options.prefix ?? "FRETE_";
7
7
  }
8
8
  key(name) {
9
9
  return this.prefix + name.replace(/[^a-zA-Z0-9]+/g, "_").toUpperCase();
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@meuecommerce/frete-adapter-node",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
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
6
  "main": "./dist/cjs/index.js",
@@ -31,7 +31,13 @@
31
31
  "access": "public"
32
32
  },
33
33
  "dependencies": {
34
- "@meuecommerce/frete": "^0.2.0"
34
+ "@meuecommerce/frete": "^0.3.1"
35
35
  },
36
- "module": "./dist/index.js"
36
+ "module": "./dist/index.js",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/devstudio163/frete.git",
40
+ "directory": "packages/adapter-node"
41
+ },
42
+ "homepage": "https://github.com/devstudio163/frete"
37
43
  }