@meuecommerce/frete-adapter-node 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/correiosHttpClient.js +31 -0
- package/dist/cjs/correiosLabelHttpClient.js +137 -0
- package/dist/cjs/envSecretStore.js +1 -1
- package/dist/cjs/inMemoryDimensionMemory.js +59 -0
- package/dist/cjs/index.js +11 -1
- package/dist/cjs/openAIDimensionEstimator.js +53 -63
- package/dist/cjs/openAIResponsesAgentRunner.js +119 -0
- package/dist/cjs/pgDimensionMemory.js +160 -0
- package/dist/correiosHttpClient.js +32 -1
- package/dist/correiosLabelHttpClient.d.ts +41 -0
- package/dist/correiosLabelHttpClient.js +134 -0
- package/dist/envSecretStore.d.ts +2 -2
- package/dist/envSecretStore.js +1 -1
- package/dist/inMemoryDimensionMemory.d.ts +10 -0
- package/dist/inMemoryDimensionMemory.js +55 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.js +5 -1
- package/dist/openAIDimensionEstimator.d.ts +24 -4
- package/dist/openAIDimensionEstimator.js +52 -63
- package/dist/openAIResponsesAgentRunner.d.ts +50 -0
- package/dist/openAIResponsesAgentRunner.js +114 -0
- package/dist/pgDimensionMemory.d.ts +62 -0
- package/dist/pgDimensionMemory.js +157 -0
- package/package.json +12 -3
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.canOverwrite = exports.PgDimensionMemory = exports.DIMENSION_MEMORY_SCHEMA_SQL = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Postgres-backed `DimensionMemory` — the production counterpart of
|
|
6
|
+
* {@link InMemoryDimensionMemory}.
|
|
7
|
+
*
|
|
8
|
+
* Two rules from the port are enforced here in SQL rather than in application
|
|
9
|
+
* code, because the memory is written from more than one place (the quote path,
|
|
10
|
+
* the label feedback, the dashboard) and a check that lives in one caller is a
|
|
11
|
+
* check that the next caller forgets:
|
|
12
|
+
*
|
|
13
|
+
* - **A weaker layer never overwrites a stronger one.** The upsert carries the
|
|
14
|
+
* layer's rank and only updates when the incoming rank is at least as strong,
|
|
15
|
+
* so an agent estimate racing with a merchant's correction loses regardless of
|
|
16
|
+
* which arrives last.
|
|
17
|
+
* - **One round trip per cart.** `get` takes every product at once, because a
|
|
18
|
+
* query per line item is exactly what makes a hot path slow.
|
|
19
|
+
*
|
|
20
|
+
* The client is the structural {@link SqlClient} rather than a `pg` import, so
|
|
21
|
+
* this package gains no runtime dependency and the tests can run real SQL
|
|
22
|
+
* against pg-mem with no network.
|
|
23
|
+
*
|
|
24
|
+
* Lookups expand to `IN ($1, $2, …)` instead of the more idiomatic
|
|
25
|
+
* `= ANY($1)`. Real Postgres accepts both, but pg-mem silently returns zero
|
|
26
|
+
* rows for the array form, and running the tests against real SQL is worth more
|
|
27
|
+
* than the tidier query — a cart is a handful of line items, so the parameter
|
|
28
|
+
* count is not a concern. Revisit if this is ever used for bulk reads.
|
|
29
|
+
*/
|
|
30
|
+
const frete_1 = require("@meuecommerce/frete");
|
|
31
|
+
Object.defineProperty(exports, "canOverwrite", { enumerable: true, get: function () { return frete_1.canOverwrite; } });
|
|
32
|
+
exports.DIMENSION_MEMORY_SCHEMA_SQL = `
|
|
33
|
+
CREATE TABLE IF NOT EXISTS dimension_memory (
|
|
34
|
+
key TEXT PRIMARY KEY,
|
|
35
|
+
instance_id TEXT NOT NULL,
|
|
36
|
+
length_cm DOUBLE PRECISION NOT NULL,
|
|
37
|
+
width_cm DOUBLE PRECISION NOT NULL,
|
|
38
|
+
height_cm DOUBLE PRECISION NOT NULL,
|
|
39
|
+
attrs JSONB,
|
|
40
|
+
confidence DOUBLE PRECISION NOT NULL,
|
|
41
|
+
source TEXT NOT NULL,
|
|
42
|
+
layer_rank INTEGER NOT NULL,
|
|
43
|
+
prompt_version TEXT,
|
|
44
|
+
model TEXT,
|
|
45
|
+
updated_at TIMESTAMPTZ NOT NULL
|
|
46
|
+
);
|
|
47
|
+
CREATE INDEX IF NOT EXISTS dimension_memory_instance_idx ON dimension_memory (instance_id);
|
|
48
|
+
`;
|
|
49
|
+
/** Lower rank = stronger layer, matching `MEMORY_LAYER_ORDER`. */
|
|
50
|
+
function layerRank(layer) {
|
|
51
|
+
const rank = frete_1.MEMORY_LAYER_ORDER.indexOf(layer);
|
|
52
|
+
// Uma camada desconhecida é tratada como a mais fraca possível, em vez de
|
|
53
|
+
// virar -1 e passar por cima de tudo.
|
|
54
|
+
return rank === -1 ? frete_1.MEMORY_LAYER_ORDER.length : rank;
|
|
55
|
+
}
|
|
56
|
+
/** `$1, $2, …` para `count` parâmetros. */
|
|
57
|
+
function placeholders(count) {
|
|
58
|
+
return Array.from({ length: count }, (_, i) => `$${i + 1}`).join(", ");
|
|
59
|
+
}
|
|
60
|
+
/** `DOUBLE PRECISION` chega como string em alguns drivers. */
|
|
61
|
+
function num(value) {
|
|
62
|
+
return typeof value === "number" ? value : Number(value);
|
|
63
|
+
}
|
|
64
|
+
function toRecord(row) {
|
|
65
|
+
const updatedAt = row.updated_at instanceof Date ? row.updated_at.toISOString() : String(row.updated_at);
|
|
66
|
+
return {
|
|
67
|
+
dims: { length: num(row.length_cm), width: num(row.width_cm), height: num(row.height_cm) },
|
|
68
|
+
...(row.attrs ? { attrs: row.attrs } : {}),
|
|
69
|
+
confidence: num(row.confidence),
|
|
70
|
+
source: row.source,
|
|
71
|
+
...(row.prompt_version ? { promptVersion: row.prompt_version } : {}),
|
|
72
|
+
...(row.model ? { model: row.model } : {}),
|
|
73
|
+
updatedAt,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
class PgDimensionMemory {
|
|
77
|
+
sql;
|
|
78
|
+
constructor(options) {
|
|
79
|
+
this.sql = options.sql;
|
|
80
|
+
}
|
|
81
|
+
/** Idempotent — safe to call on every boot. */
|
|
82
|
+
async migrate() {
|
|
83
|
+
await this.sql.query(exports.DIMENSION_MEMORY_SCHEMA_SQL);
|
|
84
|
+
}
|
|
85
|
+
async get(identities) {
|
|
86
|
+
const keys = identities.flatMap((identity) => {
|
|
87
|
+
const key = (0, frete_1.productKey)(identity);
|
|
88
|
+
return key ? [key.value] : [];
|
|
89
|
+
});
|
|
90
|
+
const found = new Map();
|
|
91
|
+
if (keys.length === 0)
|
|
92
|
+
return found;
|
|
93
|
+
const { rows } = await this.sql.query(`SELECT key, length_cm, width_cm, height_cm, attrs, confidence, source, prompt_version, model, updated_at
|
|
94
|
+
FROM dimension_memory
|
|
95
|
+
WHERE key IN (${placeholders(keys.length)})`, keys);
|
|
96
|
+
for (const row of rows)
|
|
97
|
+
found.set(row.key, toRecord(row));
|
|
98
|
+
return found;
|
|
99
|
+
}
|
|
100
|
+
async set(entries) {
|
|
101
|
+
const values = [];
|
|
102
|
+
const tuples = [];
|
|
103
|
+
for (const { identity, record } of entries) {
|
|
104
|
+
const key = (0, frete_1.productKey)(identity);
|
|
105
|
+
// Sem chave derivável não há o que gravar: inventar uma faria produtos
|
|
106
|
+
// sem relação compartilharem entrada.
|
|
107
|
+
if (!key)
|
|
108
|
+
continue;
|
|
109
|
+
const base = values.length;
|
|
110
|
+
values.push(key.value, identity.instanceId, record.dims.length, record.dims.width, record.dims.height, record.attrs ? JSON.stringify(record.attrs) : null, record.confidence, record.source, layerRank(record.source), record.promptVersion ?? null, record.model ?? null, record.updatedAt);
|
|
111
|
+
const p = (offset) => `$${base + offset}`;
|
|
112
|
+
tuples.push(`(${p(1)}, ${p(2)}, ${p(3)}, ${p(4)}, ${p(5)}, ${p(6)}::jsonb, ${p(7)}, ${p(8)}, ${p(9)}, ${p(10)}, ${p(11)}, ${p(12)}::timestamptz)`);
|
|
113
|
+
}
|
|
114
|
+
if (tuples.length === 0)
|
|
115
|
+
return;
|
|
116
|
+
await this.sql.query(`INSERT INTO dimension_memory
|
|
117
|
+
(key, instance_id, length_cm, width_cm, height_cm, attrs, confidence, source, layer_rank, prompt_version, model, updated_at)
|
|
118
|
+
VALUES ${tuples.join(", ")}
|
|
119
|
+
ON CONFLICT (key) DO UPDATE SET
|
|
120
|
+
length_cm = EXCLUDED.length_cm,
|
|
121
|
+
width_cm = EXCLUDED.width_cm,
|
|
122
|
+
height_cm = EXCLUDED.height_cm,
|
|
123
|
+
attrs = EXCLUDED.attrs,
|
|
124
|
+
confidence = EXCLUDED.confidence,
|
|
125
|
+
source = EXCLUDED.source,
|
|
126
|
+
layer_rank = EXCLUDED.layer_rank,
|
|
127
|
+
prompt_version = EXCLUDED.prompt_version,
|
|
128
|
+
model = EXCLUDED.model,
|
|
129
|
+
updated_at = EXCLUDED.updated_at
|
|
130
|
+
WHERE EXCLUDED.layer_rank <= dimension_memory.layer_rank`, values);
|
|
131
|
+
}
|
|
132
|
+
async invalidate(identities) {
|
|
133
|
+
const keys = identities.flatMap((identity) => {
|
|
134
|
+
const key = (0, frete_1.productKey)(identity);
|
|
135
|
+
return key ? [key.value] : [];
|
|
136
|
+
});
|
|
137
|
+
if (keys.length === 0)
|
|
138
|
+
return;
|
|
139
|
+
await this.sql.query(`DELETE FROM dimension_memory WHERE key IN (${placeholders(keys.length)})`, keys);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Drop agent estimates produced by an older prompt.
|
|
143
|
+
*
|
|
144
|
+
* Only touches `L3`: what the merchant declared and what a real label proved
|
|
145
|
+
* do not go stale because we changed a prompt. Meant to run in background
|
|
146
|
+
* after a prompt bump — never on the quote path.
|
|
147
|
+
*/
|
|
148
|
+
async invalidateStaleEstimates(currentPromptVersion) {
|
|
149
|
+
const { rows } = await this.sql.query(`DELETE FROM dimension_memory
|
|
150
|
+
WHERE source = 'L3' AND (prompt_version IS NULL OR prompt_version <> $1)
|
|
151
|
+
RETURNING key`, [currentPromptVersion]);
|
|
152
|
+
return rows.length;
|
|
153
|
+
}
|
|
154
|
+
/** Forget everything for one merchant — uninstall, or a support reset. */
|
|
155
|
+
async forgetInstance(instanceId) {
|
|
156
|
+
const { rows } = await this.sql.query(`DELETE FROM dimension_memory WHERE instance_id = $1 RETURNING key`, [instanceId]);
|
|
157
|
+
return rows.length;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
exports.PgDimensionMemory = PgDimensionMemory;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* The only host dependency is `fetch`, injectable for tests.
|
|
9
9
|
*/
|
|
10
|
-
import { correiosApiUrl, } from "@meuecommerce/frete";
|
|
10
|
+
import { correiosApiUrl, correiosSroPath, } from "@meuecommerce/frete";
|
|
11
11
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
12
12
|
function base64(input) {
|
|
13
13
|
return Buffer.from(input, "utf-8").toString("base64");
|
|
@@ -112,5 +112,36 @@ export function createCorreiosHttpClient(options = {}) {
|
|
|
112
112
|
getTime(payload, token) {
|
|
113
113
|
return estimate("/prazo/v1/nacional", payload, token, "correios.getTime");
|
|
114
114
|
},
|
|
115
|
+
async track(codes, token) {
|
|
116
|
+
if (!codes.length)
|
|
117
|
+
return [];
|
|
118
|
+
// SRO Rastro takes the codes as repeated `codigosObjetos` query params and
|
|
119
|
+
// `resultado=T` for the full event history (vs. `U` for the latest only).
|
|
120
|
+
// The language MUST go in the `Accept-Language` header (pt-BR/en/es-ES) —
|
|
121
|
+
// an `idioma` query param is rejected with SRO-018.
|
|
122
|
+
const params = new URLSearchParams();
|
|
123
|
+
for (const code of codes)
|
|
124
|
+
params.append("codigosObjetos", code);
|
|
125
|
+
params.append("resultado", "T");
|
|
126
|
+
const url = `${baseUrl}${correiosSroPath}?${params.toString()}`;
|
|
127
|
+
const response = await fetchWithRetry(url, {
|
|
128
|
+
method: "GET",
|
|
129
|
+
headers: {
|
|
130
|
+
Accept: "application/json",
|
|
131
|
+
"Accept-Language": "pt-BR",
|
|
132
|
+
Authorization: `Bearer ${token}`,
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
if (response.status === 401) {
|
|
136
|
+
throw { status: 401, message: "unauthorized" };
|
|
137
|
+
}
|
|
138
|
+
if (response.status !== 200) {
|
|
139
|
+
const json = await response.json().catch(() => ({}));
|
|
140
|
+
logger.warn(`[correios.track] invalid response. status=${response.status} body=${JSON.stringify(json)}`);
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
const body = (await response.json());
|
|
144
|
+
return Array.isArray(body?.objetos) ? body.objetos : [];
|
|
145
|
+
},
|
|
115
146
|
};
|
|
116
147
|
}
|
|
@@ -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,134 @@
|
|
|
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
|
+
/**
|
|
21
|
+
* Some Correios label messages are transient: the pré-postagem is fine and the
|
|
22
|
+
* PDF is still being produced, so a later poll succeeds (e.g. PPN-291 "O rótulo
|
|
23
|
+
* ainda não foi gerado. Por favor, consulte novamente."). Distinguish those from
|
|
24
|
+
* terminal ones — notably PPN-288 "status Pendente" — which won't clear by
|
|
25
|
+
* polling and must be surfaced. Retryable → keep polling; terminal → `message`.
|
|
26
|
+
*/
|
|
27
|
+
function isRetryableLabelMessage(msg) {
|
|
28
|
+
return /consulte novamente|ainda n[ãa]o foi gerado|n[ãa]o (foi )?gerado|PPN-291/i.test(msg);
|
|
29
|
+
}
|
|
30
|
+
export function createCorreiosLabelHttpClient(options = {}) {
|
|
31
|
+
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;
|
|
32
|
+
if (typeof fetch !== "function") {
|
|
33
|
+
throw new Error("createCorreiosLabelHttpClient: no fetch available (Node 18+ or pass options.fetch)");
|
|
34
|
+
}
|
|
35
|
+
async function fetchWithRetry(url, init) {
|
|
36
|
+
let lastError;
|
|
37
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
38
|
+
const controller = new AbortController();
|
|
39
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
40
|
+
try {
|
|
41
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
lastError = error;
|
|
45
|
+
if (attempt === retries - 1)
|
|
46
|
+
break;
|
|
47
|
+
await sleep(retryDelayMs * Math.pow(2, attempt));
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
clearTimeout(timer);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
throw lastError;
|
|
54
|
+
}
|
|
55
|
+
function authHeaders(token) {
|
|
56
|
+
return {
|
|
57
|
+
"Content-Type": "application/json",
|
|
58
|
+
Accept: "application/json",
|
|
59
|
+
Authorization: `Bearer ${token}`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
async createPrepostagem(payload, token) {
|
|
64
|
+
const response = await fetchWithRetry(`${baseUrl}/prepostagem/v1/prepostagens`, {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: authHeaders(token),
|
|
67
|
+
body: JSON.stringify(payload),
|
|
68
|
+
});
|
|
69
|
+
// Correios returns a structured error body (with the failure reason) on 4xx;
|
|
70
|
+
// the use case filters by `txErro`, so pass it through rather than throwing.
|
|
71
|
+
const json = (await response.json().catch(() => ({})));
|
|
72
|
+
if (response.status !== 200 && response.status !== 201) {
|
|
73
|
+
logger.warn(`[correios.createPrepostagem] status=${response.status} body=${JSON.stringify(json)}`);
|
|
74
|
+
if (!json.txErro)
|
|
75
|
+
json.txErro = `http_${response.status}`;
|
|
76
|
+
}
|
|
77
|
+
return json;
|
|
78
|
+
},
|
|
79
|
+
async getPrepostagemStatus(id, token) {
|
|
80
|
+
const response = await fetchWithRetry(`${baseUrl}/prepostagem/v2/prepostagens?id=${encodeURIComponent(id)}&page=0&size=1`, { method: "GET", headers: authHeaders(token) });
|
|
81
|
+
const json = (await response.json().catch(() => ({})));
|
|
82
|
+
const item = Array.isArray(json.itens) ? json.itens[0] : undefined;
|
|
83
|
+
return item ?? null;
|
|
84
|
+
},
|
|
85
|
+
async requestLabelPdf(ids, token) {
|
|
86
|
+
const response = await fetchWithRetry(`${baseUrl}/prepostagem/v1/prepostagens/rotulo/assincrono/pdf`, {
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: authHeaders(token),
|
|
89
|
+
body: JSON.stringify({ idsPrePostagem: ids, tipoRotulo }),
|
|
90
|
+
});
|
|
91
|
+
const json = (await response.json().catch(() => ({})));
|
|
92
|
+
if (!json.idRecibo)
|
|
93
|
+
throw new Error(`requestLabelPdf: no idRecibo (status=${response.status})`);
|
|
94
|
+
return { idRecibo: json.idRecibo };
|
|
95
|
+
},
|
|
96
|
+
async downloadLabelPdf(idRecibo, token) {
|
|
97
|
+
const path = downloadPath.replace("{idRecibo}", encodeURIComponent(idRecibo));
|
|
98
|
+
// tipoRotulo is also accepted as a query param on the download (PPN-285).
|
|
99
|
+
const sep = path.includes("?") ? "&" : "?";
|
|
100
|
+
const url = `${baseUrl}${path}${sep}tipoRotulo=${encodeURIComponent(tipoRotulo)}`;
|
|
101
|
+
const response = await fetchWithRetry(url, {
|
|
102
|
+
method: "GET",
|
|
103
|
+
headers: { Accept: "application/pdf, application/json", Authorization: `Bearer ${token}` },
|
|
104
|
+
});
|
|
105
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
106
|
+
// 202 (or 204) = still processing; tell the use case to poll again.
|
|
107
|
+
if (response.status === 202 || response.status === 204)
|
|
108
|
+
return { ready: false };
|
|
109
|
+
if (response.status !== 200) {
|
|
110
|
+
const body = await response.text().catch(() => "");
|
|
111
|
+
// A retryable business message can arrive with a non-2xx status too.
|
|
112
|
+
if (isRetryableLabelMessage(body))
|
|
113
|
+
return { ready: false };
|
|
114
|
+
return { ready: false, message: `http_${response.status}: ${body.slice(0, 200)}` };
|
|
115
|
+
}
|
|
116
|
+
// Correios may return the PDF as raw bytes (application/pdf) or wrapped in
|
|
117
|
+
// JSON as base64 (dados/pdf). Handle both.
|
|
118
|
+
if (contentType.includes("application/pdf") || contentType.includes("octet-stream")) {
|
|
119
|
+
const buf = Buffer.from(await response.arrayBuffer());
|
|
120
|
+
return buf.length ? { ready: true, pdfBase64: buf.toString("base64") } : { ready: false };
|
|
121
|
+
}
|
|
122
|
+
const json = (await response.json().catch(() => ({})));
|
|
123
|
+
const pdfBase64 = (json.dados ?? json.pdf ?? json.base64 ?? json.arquivo);
|
|
124
|
+
if (pdfBase64)
|
|
125
|
+
return { ready: true, pdfBase64 };
|
|
126
|
+
// 200 without a PDF carries a business message (e.g. "status Pendente" or
|
|
127
|
+
// "ainda não foi gerado, consulte novamente"). Retryable ones → keep polling.
|
|
128
|
+
const message = typeof json.mensagem === "string" ? json.mensagem : undefined;
|
|
129
|
+
if (message && isRetryableLabelMessage(message))
|
|
130
|
+
return { ready: false };
|
|
131
|
+
return message ? { ready: false, message } : { ready: false };
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
package/dist/envSecretStore.d.ts
CHANGED
|
@@ -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 `
|
|
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 "
|
|
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 {
|
package/dist/envSecretStore.js
CHANGED
|
@@ -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 ?? "
|
|
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();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DimensionMemory, DimensionMemoryEntry, DimensionRecord, ProductIdentity } from "@meuecommerce/frete";
|
|
2
|
+
export declare class InMemoryDimensionMemory implements DimensionMemory {
|
|
3
|
+
private readonly records;
|
|
4
|
+
constructor(seed?: DimensionMemoryEntry[]);
|
|
5
|
+
/** How many products the memory holds. For tests and local inspection. */
|
|
6
|
+
get size(): number;
|
|
7
|
+
get(identities: ProductIdentity[]): Promise<Map<string, DimensionRecord>>;
|
|
8
|
+
set(entries: DimensionMemoryEntry[]): Promise<void>;
|
|
9
|
+
invalidate(identities: ProductIdentity[]): Promise<void>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory `DimensionMemory` — local dev, the MCP server's local mode, and
|
|
3
|
+
* tests. Production swaps it for the Postgres-backed one; the core does not
|
|
4
|
+
* change.
|
|
5
|
+
*
|
|
6
|
+
* It enforces the layer rule rather than just storing what it is given: an
|
|
7
|
+
* agent guess (`L3`) must never overwrite what the merchant declared (`L0`).
|
|
8
|
+
* Putting that here as well as in the Postgres implementation is deliberate —
|
|
9
|
+
* it is a property of the memory, not of one backend, and the tests that pin it
|
|
10
|
+
* run against this one.
|
|
11
|
+
*/
|
|
12
|
+
import { canOverwrite, productKey } from "@meuecommerce/frete";
|
|
13
|
+
export class InMemoryDimensionMemory {
|
|
14
|
+
records = new Map();
|
|
15
|
+
constructor(seed = []) {
|
|
16
|
+
// O seed passa pela mesma regra de camada que uma escrita normal.
|
|
17
|
+
void this.set(seed);
|
|
18
|
+
}
|
|
19
|
+
/** How many products the memory holds. For tests and local inspection. */
|
|
20
|
+
get size() {
|
|
21
|
+
return this.records.size;
|
|
22
|
+
}
|
|
23
|
+
async get(identities) {
|
|
24
|
+
const found = new Map();
|
|
25
|
+
for (const identity of identities) {
|
|
26
|
+
const key = productKey(identity);
|
|
27
|
+
if (!key)
|
|
28
|
+
continue;
|
|
29
|
+
const record = this.records.get(key.value);
|
|
30
|
+
if (record)
|
|
31
|
+
found.set(key.value, record);
|
|
32
|
+
}
|
|
33
|
+
return found;
|
|
34
|
+
}
|
|
35
|
+
async set(entries) {
|
|
36
|
+
for (const { identity, record } of entries) {
|
|
37
|
+
const key = productKey(identity);
|
|
38
|
+
// Sem chave derivável não há o que gravar: inventar uma faria produtos
|
|
39
|
+
// sem relação compartilharem entrada.
|
|
40
|
+
if (!key)
|
|
41
|
+
continue;
|
|
42
|
+
const existing = this.records.get(key.value);
|
|
43
|
+
if (existing && !canOverwrite(existing.source, record.source))
|
|
44
|
+
continue;
|
|
45
|
+
this.records.set(key.value, record);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async invalidate(identities) {
|
|
49
|
+
for (const identity of identities) {
|
|
50
|
+
const key = productKey(identity);
|
|
51
|
+
if (key)
|
|
52
|
+
this.records.delete(key.value);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,11 +6,18 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export { createCorreiosHttpClient } from "./correiosHttpClient.js";
|
|
8
8
|
export type { CorreiosHttpClientOptions } from "./correiosHttpClient.js";
|
|
9
|
+
export { createCorreiosLabelHttpClient } from "./correiosLabelHttpClient.js";
|
|
10
|
+
export type { CorreiosLabelHttpClientOptions } from "./correiosLabelHttpClient.js";
|
|
9
11
|
export { InMemorySettingsStore } from "./inMemorySettingsStore.js";
|
|
10
12
|
export type { InMemorySettingsStoreOptions } from "./inMemorySettingsStore.js";
|
|
13
|
+
export { InMemoryDimensionMemory } from "./inMemoryDimensionMemory.js";
|
|
11
14
|
export { EnvSecretStore } from "./envSecretStore.js";
|
|
12
15
|
export type { EnvSecretStoreOptions } from "./envSecretStore.js";
|
|
13
|
-
export { createOpenAIDimensionEstimator } from "./openAIDimensionEstimator.js";
|
|
16
|
+
export { DIMENSION_PROMPT_VERSION, createOpenAIDimensionEstimator } from "./openAIDimensionEstimator.js";
|
|
17
|
+
export { createOpenAIResponsesAgentRunner } from "./openAIResponsesAgentRunner.js";
|
|
18
|
+
export { DIMENSION_MEMORY_SCHEMA_SQL, PgDimensionMemory } from "./pgDimensionMemory.js";
|
|
19
|
+
export type { PgDimensionMemoryOptions, SqlClient } from "./pgDimensionMemory.js";
|
|
20
|
+
export type { OpenAIResponsesAgentRunnerOptions } from "./openAIResponsesAgentRunner.js";
|
|
14
21
|
export type { OpenAIDimensionEstimatorOptions } from "./openAIDimensionEstimator.js";
|
|
15
22
|
export { createOpenAIBoxDistributor } from "./openAIBoxDistributor.js";
|
|
16
23
|
export type { OpenAIBoxDistributorOptions } from "./openAIBoxDistributor.js";
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
* Shopify/Fly deployment today, and the MCP server next.
|
|
6
6
|
*/
|
|
7
7
|
export { createCorreiosHttpClient } from "./correiosHttpClient.js";
|
|
8
|
+
export { createCorreiosLabelHttpClient } from "./correiosLabelHttpClient.js";
|
|
8
9
|
export { InMemorySettingsStore } from "./inMemorySettingsStore.js";
|
|
10
|
+
export { InMemoryDimensionMemory } from "./inMemoryDimensionMemory.js";
|
|
9
11
|
export { EnvSecretStore } from "./envSecretStore.js";
|
|
10
|
-
export { createOpenAIDimensionEstimator } from "./openAIDimensionEstimator.js";
|
|
12
|
+
export { DIMENSION_PROMPT_VERSION, createOpenAIDimensionEstimator } from "./openAIDimensionEstimator.js";
|
|
13
|
+
export { createOpenAIResponsesAgentRunner } from "./openAIResponsesAgentRunner.js";
|
|
14
|
+
export { DIMENSION_MEMORY_SCHEMA_SQL, PgDimensionMemory } from "./pgDimensionMemory.js";
|
|
11
15
|
export { createOpenAIBoxDistributor } from "./openAIBoxDistributor.js";
|
|
@@ -1,24 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `ProductDimensionEstimator` implemented with the OpenAI
|
|
2
|
+
* `ProductDimensionEstimator` implemented with the OpenAI Responses API and a
|
|
3
|
+
* strict structured output.
|
|
3
4
|
*
|
|
4
5
|
* Ports the dimension-estimation path from the Velo `openAI/` modules:
|
|
5
|
-
* - `ai-tools.js` ->
|
|
6
|
+
* - `ai-tools.js` -> request/response handling (was a forced tool call)
|
|
6
7
|
* - `prompt-methods.js`-> `createDimensionEstimationPrompt`
|
|
7
8
|
* - `box-methods.js` -> `validateAndOptimizeDimensions` (post-process AI output)
|
|
8
9
|
*
|
|
10
|
+
* Why the move off chat-completions with a forced tool call: a tool call is a
|
|
11
|
+
* request for a shape, not a guarantee of one — the arguments still arrive as a
|
|
12
|
+
* string that can be truncated or malformed, which is why the old code had to
|
|
13
|
+
* `JSON.parse` and hope. `text.format` with `strict: true` makes the schema a
|
|
14
|
+
* constraint on decoding instead, so a missing field or an extra one cannot
|
|
15
|
+
* come back at all.
|
|
16
|
+
*
|
|
9
17
|
* `fetch` is injectable (defaults to global fetch) so this also runs under Velo
|
|
10
18
|
* with `wix-fetch` passed in. The API key is passed in — the caller resolves it
|
|
11
19
|
* (e.g. via a `SecretStore`), keeping secret handling out of this module.
|
|
12
20
|
*/
|
|
13
|
-
import type { ProductDimensionEstimator } from "@meuecommerce/frete";
|
|
21
|
+
import type { LlmAgentRunner, ProductDimensionEstimator } from "@meuecommerce/frete";
|
|
14
22
|
type FetchLike = typeof globalThis.fetch;
|
|
23
|
+
/**
|
|
24
|
+
* Bump whenever the prompt or the schema changes. It is written next to every
|
|
25
|
+
* `L3` record so a prompt change can invalidate stale estimates in background
|
|
26
|
+
* instead of at checkout.
|
|
27
|
+
*/
|
|
28
|
+
export declare const DIMENSION_PROMPT_VERSION = "2026-09-04.responses.v1";
|
|
15
29
|
export interface OpenAIDimensionEstimatorOptions {
|
|
16
|
-
apiKey
|
|
30
|
+
apiKey?: string;
|
|
17
31
|
fetch?: FetchLike;
|
|
18
32
|
model?: string;
|
|
19
33
|
temperature?: number;
|
|
20
34
|
apiUrl?: string;
|
|
21
35
|
timeoutMs?: number;
|
|
36
|
+
/**
|
|
37
|
+
* Run the call through an existing {@link LlmAgentRunner} instead of building
|
|
38
|
+
* one. Pass it to share a runner across use cases, or to substitute the
|
|
39
|
+
* transport in tests; omit it and one is created from `apiKey`.
|
|
40
|
+
*/
|
|
41
|
+
runner?: LlmAgentRunner;
|
|
22
42
|
}
|
|
23
43
|
export declare function createOpenAIDimensionEstimator(options: OpenAIDimensionEstimatorOptions): ProductDimensionEstimator;
|
|
24
44
|
export {};
|