@fayz-ai/plugin-fiscal-br 0.8.1 → 0.9.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/chunk-5LI2FDOM.js +231 -0
- package/dist/chunk-5LI2FDOM.js.map +1 -0
- package/dist/index.js +31 -20
- package/dist/index.js.map +1 -1
- package/dist/migrations/index.d.ts +1 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/server/index.d.ts +7 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +3 -0
- package/dist/server/index.js.map +1 -0
- package/package.json +9 -5
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// src/lib/note-suggestion.ts
|
|
2
|
+
var ENTRY_TYPES = ["insumo", "revenda", "patrimonio", "uso_consumo", "despesa", "servico"];
|
|
3
|
+
var PACKAGING_COUNT = {
|
|
4
|
+
MI: 1e3,
|
|
5
|
+
MIL: 1e3,
|
|
6
|
+
MLH: 1e3,
|
|
7
|
+
MILHEIRO: 1e3,
|
|
8
|
+
"MI/1000": 1e3,
|
|
9
|
+
DZ: 12,
|
|
10
|
+
DUZIA: 12,
|
|
11
|
+
"D\xDAZIA": 12,
|
|
12
|
+
CENTO: 100,
|
|
13
|
+
CENT: 100
|
|
14
|
+
};
|
|
15
|
+
var COUNTING_UNITS = /* @__PURE__ */ new Set(["UN", "UND", "UNID", "UNIDADE", "PC", "P\xC7", "PCS", "PEC", "PE\xC7A", "PECA"]);
|
|
16
|
+
var norm = (u) => (u ?? "").trim().toUpperCase().replace(/\.$/, "");
|
|
17
|
+
function packagingCount(commercialUnit) {
|
|
18
|
+
return PACKAGING_COUNT[norm(commercialUnit)] ?? null;
|
|
19
|
+
}
|
|
20
|
+
var isCountingUnit = (abbreviation) => COUNTING_UNITS.has(norm(abbreviation));
|
|
21
|
+
function taxableFactor(item, stockUnitAbbreviation) {
|
|
22
|
+
const trib = norm(item.unitTaxable);
|
|
23
|
+
const estoque = norm(stockUnitAbbreviation);
|
|
24
|
+
if (!trib || !estoque || trib !== estoque) return null;
|
|
25
|
+
if (trib === norm(item.unit)) return null;
|
|
26
|
+
if (!item.quantityTaxable || item.quantity <= 0) return null;
|
|
27
|
+
const fator = item.quantityTaxable / item.quantity;
|
|
28
|
+
return fator > 0 ? Math.round(fator * 1e4) / 1e4 : null;
|
|
29
|
+
}
|
|
30
|
+
function enforceFactorEvidence(result, items, units) {
|
|
31
|
+
const avisos = [...result.warnings];
|
|
32
|
+
const byId = new Map(items.map((i) => [i.id, i]));
|
|
33
|
+
const unitAbbr = new Map(units.map((u) => [u.id, u.abbreviation]));
|
|
34
|
+
const corrigidos = result.items.map((s) => {
|
|
35
|
+
const item = byId.get(s.itemId);
|
|
36
|
+
if (!item) return s;
|
|
37
|
+
const sigla = s.stockUnitId ? unitAbbr.get(s.stockUnitId) : null;
|
|
38
|
+
const doXml = taxableFactor(item, sigla);
|
|
39
|
+
if (doXml !== null) return { ...s, conversionFactor: doXml };
|
|
40
|
+
const conteudo = packagingCount(item.unit);
|
|
41
|
+
if (!conteudo || s.conversionFactor !== 1) return s;
|
|
42
|
+
if (isCountingUnit(sigla)) return { ...s, conversionFactor: conteudo };
|
|
43
|
+
if (!sigla) return { ...s, conversionFactor: conteudo, confidence: Math.min(s.confidence, 0.5) };
|
|
44
|
+
avisos.push(
|
|
45
|
+
`\xAB${item.description}\xBB vem em ${norm(item.unit)} (${conteudo} por embalagem) e o estoque \xE9 em ${norm(sigla)}: confira o fator de convers\xE3o.`
|
|
46
|
+
);
|
|
47
|
+
return { ...s, confidence: Math.min(s.confidence, 0.3) };
|
|
48
|
+
});
|
|
49
|
+
return { items: corrigidos, warnings: avisos };
|
|
50
|
+
}
|
|
51
|
+
var MESSAGE_BUDGET = 24e3;
|
|
52
|
+
var SUGGESTION_PROMPT = [
|
|
53
|
+
"Voc\xEA associa itens de uma NF-e de compra aos produtos cadastrados da empresa.",
|
|
54
|
+
"Responda SEMPRE pela tool `aplicar_sugestoes`. Tudo \xE9 NUMERADO: use s\xF3 n\xFAmeros que",
|
|
55
|
+
"apareceram nos dados. `c` \xE9 o n\xFAmero de um candidato DAQUELE item.",
|
|
56
|
+
"Sem candidato bom (semelhan\xE7a abaixo de 0,6) ou lista vazia: c=null e `novo` com o nome.",
|
|
57
|
+
"",
|
|
58
|
+
"f \u2014 fator de convers\xE3o (unidade comercial da nota \xD7 unidade de estoque):",
|
|
59
|
+
"- EVID\xCANCIA MAIS FORTE: `utrib`/`qtrib` no item s\xE3o a unidade e a quantidade tribut\xE1veis do",
|
|
60
|
+
" XML. Se `utrib` for a unidade em que o produto \xE9 guardado, f = qtrib \xF7 q (4 CX com qtrib",
|
|
61
|
+
" 88,224 KG \u2192 f = 22,056). Quando isso vale, n\xE3o h\xE1 o que julgar.",
|
|
62
|
+
"- Embalagem (CX, FD, SC, CJ, PC, PCT, EMB, DZ, MI) com estoque em peso ou pe\xE7a: f NUNCA \xE9 1.",
|
|
63
|
+
' Descubra quanto cada embalagem cont\xE9m pela descri\xE7\xE3o ("CG 5,5/8,5KG", "BALDE 3KG" \u2192 3).',
|
|
64
|
+
"- `emb` no item \xE9 o conte\xFAdo que a pr\xF3pria sigla declara e n\xE3o se discute: MI (milheiro)",
|
|
65
|
+
" s\xE3o 1.000, DZ s\xE3o 12, CENTO s\xE3o 100. Com estoque em pe\xE7a, f \xE9 exatamente esse n\xFAmero.",
|
|
66
|
+
"- Confira o custo: custo por kg = t \xF7 (q \xD7 f). Absurdo para o tipo de produto (carne a",
|
|
67
|
+
" R$ 397/kg) significa f errado.",
|
|
68
|
+
"- Sem evid\xEAncia de peso: f=1 e conf no m\xE1ximo 0,4 \u2014 melhor revisar que entrar errado.",
|
|
69
|
+
"",
|
|
70
|
+
"a \u2014 plano de contas:",
|
|
71
|
+
"- A conta MAIS ESPEC\xCDFICA cujo nome represente a natureza do item.",
|
|
72
|
+
"- NUNCA a mesma conta para todos: cada item \xE9 avaliado sozinho.",
|
|
73
|
+
'- Limpeza/papel toalha \u2192 "Material de limpeza"; secos \u2192 "Mercearia"; bebidas \u2192 "Bebidas";',
|
|
74
|
+
' embalagem \u2192 "Embalagens"; equipamento \u2192 "Imobilizado". Sem conta razo\xE1vel: a=null.',
|
|
75
|
+
"",
|
|
76
|
+
"tipo: uso_consumo \xE9 o que a casa gasta sem vender nem transformar (luva, descart\xE1vel,",
|
|
77
|
+
" limpeza) e ENTRA no estoque. despesa e servico N\xC3O viram estoque. S\xF3 para frete, servi\xE7o, taxa.",
|
|
78
|
+
"avisos: o que ficou sem evid\xEAncia. Vazio quando n\xE3o houve nenhum."
|
|
79
|
+
].join("\n");
|
|
80
|
+
var SUGGESTION_TOOL = {
|
|
81
|
+
type: "function",
|
|
82
|
+
function: {
|
|
83
|
+
name: "aplicar_sugestoes",
|
|
84
|
+
description: "Associa\xE7\xF5es sugeridas para cada item numerado da NF-e",
|
|
85
|
+
parameters: {
|
|
86
|
+
type: "object",
|
|
87
|
+
properties: {
|
|
88
|
+
r: {
|
|
89
|
+
type: "array",
|
|
90
|
+
items: {
|
|
91
|
+
type: "object",
|
|
92
|
+
properties: {
|
|
93
|
+
it: { type: "integer", description: "n\xFAmero do item" },
|
|
94
|
+
tipo: { type: "string", enum: ["insumo", "revenda", "patrimonio", "uso_consumo", "despesa", "servico"] },
|
|
95
|
+
c: { type: ["integer", "null"], description: "n\xFAmero do candidato daquele item" },
|
|
96
|
+
novo: { type: ["string", "null"], description: "nome do produto a criar" },
|
|
97
|
+
a: { type: ["integer", "null"], description: "n\xFAmero da conta" },
|
|
98
|
+
f: { type: "number", description: "fator de convers\xE3o" },
|
|
99
|
+
conf: { type: "number", description: "0 a 1" }
|
|
100
|
+
},
|
|
101
|
+
required: ["it", "tipo", "f", "conf"]
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
avisos: { type: "array", items: { type: "string" } }
|
|
105
|
+
},
|
|
106
|
+
required: ["r"]
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
var num = (v, fallback = 0) => {
|
|
111
|
+
const n = typeof v === "string" ? Number(v.replace(",", ".")) : Number(v);
|
|
112
|
+
return Number.isFinite(n) ? n : fallback;
|
|
113
|
+
};
|
|
114
|
+
var str = (v) => {
|
|
115
|
+
const s = typeof v === "string" ? v.trim() : "";
|
|
116
|
+
return s ? s : null;
|
|
117
|
+
};
|
|
118
|
+
var idx = (v, length) => {
|
|
119
|
+
const n = typeof v === "number" ? v : Number(v);
|
|
120
|
+
return Number.isInteger(n) && n >= 1 && n <= length ? n : null;
|
|
121
|
+
};
|
|
122
|
+
function resolveStockUnit(candidate, units) {
|
|
123
|
+
const alvo = norm(candidate?.unit);
|
|
124
|
+
if (!alvo) return null;
|
|
125
|
+
return units.find((u) => norm(u.abbreviation) === alvo || norm(u.name) === alvo)?.id ?? null;
|
|
126
|
+
}
|
|
127
|
+
function buildSuggestionPayload(book, note) {
|
|
128
|
+
return JSON.stringify({
|
|
129
|
+
nota: { n: note.number, forn: note.supplierName, nat: note.natureOperation },
|
|
130
|
+
// `a` é o número da conta; `n`, o nome. Código fiscal fora: o modelo escolhe
|
|
131
|
+
// pelo nome, e o código eram 8 caracteres por linha sem efeito na escolha.
|
|
132
|
+
contas: book.accounts.map((a, i) => ({ a: i + 1, n: a.name })),
|
|
133
|
+
itens: book.items.map((item, i) => {
|
|
134
|
+
const emb = packagingCount(item.unit);
|
|
135
|
+
return {
|
|
136
|
+
it: i + 1,
|
|
137
|
+
d: item.description,
|
|
138
|
+
un: item.unit,
|
|
139
|
+
q: item.quantity,
|
|
140
|
+
vu: item.unitPrice,
|
|
141
|
+
t: item.total,
|
|
142
|
+
...item.ncm ? { ncm: item.ncm } : {},
|
|
143
|
+
...emb ? { emb } : {},
|
|
144
|
+
// A evidência de peso que o V1 lia do XML com regex e a V2 já tem em
|
|
145
|
+
// coluna. Só viaja quando diz algo novo: unidade tributável igual à
|
|
146
|
+
// comercial é fator 1, e repeti-la seria ruído em 3.100 das 3.675 linhas.
|
|
147
|
+
...item.unitTaxable && norm(item.unitTaxable) !== norm(item.unit) ? { utrib: item.unitTaxable, qtrib: item.quantityTaxable } : {},
|
|
148
|
+
c: (book.candidates[i] ?? []).map((cand, j) => ({ c: j + 1, n: cand.name }))
|
|
149
|
+
};
|
|
150
|
+
})
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
var suggestionSize = (book, note) => SUGGESTION_PROMPT.length + buildSuggestionPayload(book, note).length;
|
|
154
|
+
function splitToFit(input, budget = MESSAGE_BUDGET) {
|
|
155
|
+
const bookOf = (items) => ({
|
|
156
|
+
items,
|
|
157
|
+
candidates: items.map((i) => input.candidates[i.id] ?? []),
|
|
158
|
+
accounts: input.accounts,
|
|
159
|
+
units: input.units
|
|
160
|
+
});
|
|
161
|
+
const lotes = [];
|
|
162
|
+
const fila = [input.items];
|
|
163
|
+
while (fila.length) {
|
|
164
|
+
const atual = fila.shift();
|
|
165
|
+
if (atual.length === 0) continue;
|
|
166
|
+
const book = bookOf(atual);
|
|
167
|
+
if (atual.length === 1 || suggestionSize(book, input.note) <= budget) {
|
|
168
|
+
lotes.push(book);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const meio = Math.ceil(atual.length / 2);
|
|
172
|
+
fila.unshift(atual.slice(0, meio), atual.slice(meio));
|
|
173
|
+
}
|
|
174
|
+
return lotes;
|
|
175
|
+
}
|
|
176
|
+
function stripFence(text) {
|
|
177
|
+
const trimmed = text.trim();
|
|
178
|
+
const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
179
|
+
if (fenced) return fenced[1].trim();
|
|
180
|
+
const start = trimmed.indexOf("{");
|
|
181
|
+
const end = trimmed.lastIndexOf("}");
|
|
182
|
+
return start >= 0 && end > start ? trimmed.slice(start, end + 1) : trimmed;
|
|
183
|
+
}
|
|
184
|
+
function parseNoteSuggestionReply(reply, book) {
|
|
185
|
+
let raw;
|
|
186
|
+
if (typeof reply === "string") {
|
|
187
|
+
try {
|
|
188
|
+
raw = JSON.parse(stripFence(reply));
|
|
189
|
+
} catch {
|
|
190
|
+
return { items: [], warnings: ["N\xE3o consegui entender o que voltou da sugest\xE3o."] };
|
|
191
|
+
}
|
|
192
|
+
} else if (reply && typeof reply === "object") {
|
|
193
|
+
raw = reply;
|
|
194
|
+
} else {
|
|
195
|
+
return { items: [], warnings: ["A sugest\xE3o voltou vazia."] };
|
|
196
|
+
}
|
|
197
|
+
const linhas = Array.isArray(raw.r) ? raw.r : [];
|
|
198
|
+
const items = [];
|
|
199
|
+
for (const linha of linhas) {
|
|
200
|
+
const row = linha ?? {};
|
|
201
|
+
const nItem = idx(row.it, book.items.length);
|
|
202
|
+
if (nItem === null) continue;
|
|
203
|
+
const item = book.items[nItem - 1];
|
|
204
|
+
const candidatos = book.candidates[nItem - 1] ?? [];
|
|
205
|
+
const nCand = idx(row.c, candidatos.length);
|
|
206
|
+
const escolhido = nCand === null ? void 0 : candidatos[nCand - 1];
|
|
207
|
+
const nConta = idx(row.a, book.accounts.length);
|
|
208
|
+
const tipo = str(row.tipo);
|
|
209
|
+
const fator = num(row.f, 1);
|
|
210
|
+
items.push({
|
|
211
|
+
itemId: item.id,
|
|
212
|
+
entryType: ENTRY_TYPES.includes(tipo) ? tipo : null,
|
|
213
|
+
productId: escolhido?.id ?? null,
|
|
214
|
+
// O prompt pede `novo` quando nada casa, e o modelo às vezes devolve null —
|
|
215
|
+
// aí a tela ficava sem produto E sem nome para oferecer. A descrição do
|
|
216
|
+
// item é o padrão honesto, e é o que o V1 gravava em `ai_product_name`.
|
|
217
|
+
newProductName: escolhido ? null : str(row.novo) ?? str(item.description),
|
|
218
|
+
stockUnitId: resolveStockUnit(escolhido, book.units),
|
|
219
|
+
// Fator zero ou negativo divide o custo unitário por zero: 1 é honesto.
|
|
220
|
+
conversionFactor: fator > 0 ? fator : 1,
|
|
221
|
+
chartAccountId: nConta === null ? null : book.accounts[nConta - 1].id,
|
|
222
|
+
confidence: Math.min(1, Math.max(0, num(row.conf, 0)))
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
const warnings = Array.isArray(raw.avisos) ? raw.avisos.map((w) => str(w)).filter((w) => !!w) : [];
|
|
226
|
+
return { items, warnings };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export { MESSAGE_BUDGET, SUGGESTION_PROMPT, SUGGESTION_TOOL, buildSuggestionPayload, enforceFactorEvidence, isCountingUnit, packagingCount, parseNoteSuggestionReply, splitToFit };
|
|
230
|
+
//# sourceMappingURL=chunk-5LI2FDOM.js.map
|
|
231
|
+
//# sourceMappingURL=chunk-5LI2FDOM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/note-suggestion.ts"],"names":[],"mappings":";AA0BA,IAAM,cAAyC,CAAC,QAAA,EAAU,WAAW,YAAA,EAAc,aAAA,EAAe,WAAW,SAAS,CAAA;AAStH,IAAM,eAAA,GAA0C;AAAA,EAC9C,EAAA,EAAI,GAAA;AAAA,EAAM,GAAA,EAAK,GAAA;AAAA,EAAM,GAAA,EAAK,GAAA;AAAA,EAAM,QAAA,EAAU,GAAA;AAAA,EAAM,SAAA,EAAW,GAAA;AAAA,EAC3D,EAAA,EAAI,EAAA;AAAA,EAAI,KAAA,EAAO,EAAA;AAAA,EAAI,UAAA,EAAS,EAAA;AAAA,EAC5B,KAAA,EAAO,GAAA;AAAA,EAAK,IAAA,EAAM;AACpB,CAAA;AAGA,IAAM,cAAA,mBAAiB,IAAI,GAAA,CAAI,CAAC,MAAM,KAAA,EAAO,MAAA,EAAQ,SAAA,EAAW,IAAA,EAAM,OAAA,EAAM,KAAA,EAAO,KAAA,EAAO,SAAA,EAAQ,MAAM,CAAC,CAAA;AAEzG,IAAM,IAAA,GAAO,CAAC,CAAA,KAAA,CACX,CAAA,IAAK,EAAA,EAAI,IAAA,EAAK,CAAE,WAAA,EAAY,CAAE,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAA;AAG3C,SAAS,eAAe,cAAA,EAA0D;AACvF,EAAA,OAAO,eAAA,CAAgB,IAAA,CAAK,cAAc,CAAC,CAAA,IAAK,IAAA;AAClD;AAEO,IAAM,iBAAiB,CAAC,YAAA,KAC7B,eAAe,GAAA,CAAI,IAAA,CAAK,YAAY,CAAC;AAgBhC,SAAS,aAAA,CACd,MACA,qBAAA,EACe;AACf,EAAA,MAAM,IAAA,GAAO,IAAA,CAAK,IAAA,CAAK,WAAW,CAAA;AAClC,EAAA,MAAM,OAAA,GAAU,KAAK,qBAAqB,CAAA;AAC1C,EAAA,IAAI,CAAC,IAAA,IAAQ,CAAC,OAAA,IAAW,IAAA,KAAS,SAAS,OAAO,IAAA;AAElD,EAAA,IAAI,IAAA,KAAS,IAAA,CAAK,IAAA,CAAK,IAAI,GAAG,OAAO,IAAA;AACrC,EAAA,IAAI,CAAC,IAAA,CAAK,eAAA,IAAmB,IAAA,CAAK,QAAA,IAAY,GAAG,OAAO,IAAA;AACxD,EAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,eAAA,GAAkB,IAAA,CAAK,QAAA;AAC1C,EAAA,OAAO,QAAQ,CAAA,GAAI,IAAA,CAAK,MAAM,KAAA,GAAQ,GAAK,IAAI,GAAA,GAAQ,IAAA;AACzD;AA6BO,SAAS,qBAAA,CACd,MAAA,EACA,KAAA,EACA,KAAA,EACsB;AACtB,EAAA,MAAM,MAAA,GAAS,CAAC,GAAG,MAAA,CAAO,QAAQ,CAAA;AAClC,EAAA,MAAM,IAAA,GAAO,IAAI,GAAA,CAAI,KAAA,CAAM,GAAA,CAAI,CAAC,CAAA,KAAM,CAAC,CAAA,CAAE,EAAA,EAAI,CAAC,CAAC,CAAC,CAAA;AAChD,EAAA,MAAM,QAAA,GAAW,IAAI,GAAA,CAAI,KAAA,CAAM,GAAA,CAAI,CAAC,CAAA,KAAM,CAAC,CAAA,CAAE,EAAA,EAAI,CAAA,CAAE,YAAY,CAAC,CAAC,CAAA;AAEjE,EAAA,MAAM,UAAA,GAAa,MAAA,CAAO,KAAA,CAAM,GAAA,CAAI,CAAC,CAAA,KAAM;AACzC,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,GAAA,CAAI,CAAA,CAAE,MAAM,CAAA;AAC9B,IAAA,IAAI,CAAC,MAAM,OAAO,CAAA;AAClB,IAAA,MAAM,QAAQ,CAAA,CAAE,WAAA,GAAc,SAAS,GAAA,CAAI,CAAA,CAAE,WAAW,CAAA,GAAI,IAAA;AAG5D,IAAA,MAAM,KAAA,GAAQ,aAAA,CAAc,IAAA,EAAM,KAAK,CAAA;AACvC,IAAA,IAAI,UAAU,IAAA,EAAM,OAAO,EAAE,GAAG,CAAA,EAAG,kBAAkB,KAAA,EAAM;AAG3D,IAAA,MAAM,QAAA,GAAW,cAAA,CAAe,IAAA,CAAK,IAAI,CAAA;AACzC,IAAA,IAAI,CAAC,QAAA,IAAY,CAAA,CAAE,gBAAA,KAAqB,GAAG,OAAO,CAAA;AAGlD,IAAA,IAAI,cAAA,CAAe,KAAK,CAAA,EAAG,OAAO,EAAE,GAAG,CAAA,EAAG,kBAAkB,QAAA,EAAS;AAKrE,IAAA,IAAI,CAAC,KAAA,EAAO,OAAO,EAAE,GAAG,CAAA,EAAG,gBAAA,EAAkB,QAAA,EAAU,UAAA,EAAY,IAAA,CAAK,GAAA,CAAI,CAAA,CAAE,UAAA,EAAY,GAAG,CAAA,EAAE;AAI/F,IAAA,MAAA,CAAO,IAAA;AAAA,MACL,CAAA,IAAA,EAAI,IAAA,CAAK,WAAW,CAAA,YAAA,EAAY,IAAA,CAAK,IAAA,CAAK,IAAI,CAAC,CAAA,EAAA,EAAK,QAAQ,CAAA,oCAAA,EACtC,IAAA,CAAK,KAAK,CAAC,CAAA,kCAAA;AAAA,KACnC;AACA,IAAA,OAAO,EAAE,GAAG,CAAA,EAAG,UAAA,EAAY,KAAK,GAAA,CAAI,CAAA,CAAE,UAAA,EAAY,GAAG,CAAA,EAAE;AAAA,EACzD,CAAC,CAAA;AAED,EAAA,OAAO,EAAE,KAAA,EAAO,UAAA,EAAY,QAAA,EAAU,MAAA,EAAO;AAC/C;AAgFO,IAAM,cAAA,GAAiB;AAEvB,IAAM,iBAAA,GAAoB;AAAA,EAC/B,kFAAA;AAAA,EACA,6FAAA;AAAA,EACA,0EAAA;AAAA,EACA,6FAAA;AAAA,EACA,EAAA;AAAA,EACA,qFAAA;AAAA,EACA,sGAAA;AAAA,EACA,kGAAA;AAAA,EACA,8EAAA;AAAA,EACA,oGAAA;AAAA,EACA,yGAAA;AAAA,EACA,sGAAA;AAAA,EACA,2GAAA;AAAA,EACA,8FAAA;AAAA,EACA,kCAAA;AAAA,EACA,kGAAA;AAAA,EACA,EAAA;AAAA,EACA,2BAAA;AAAA,EACA,uEAAA;AAAA,EACA,oEAAA;AAAA,EACA,0GAAA;AAAA,EACA,mGAAA;AAAA,EACA,EAAA;AAAA,EACA,6FAAA;AAAA,EACA,4GAAA;AAAA,EACA;AACF,CAAA,CAAE,KAAK,IAAI;AAOJ,IAAM,eAAA,GAAkB;AAAA,EAC7B,IAAA,EAAM,UAAA;AAAA,EACN,QAAA,EAAU;AAAA,IACR,IAAA,EAAM,mBAAA;AAAA,IACN,WAAA,EAAa,6DAAA;AAAA,IACb,UAAA,EAAY;AAAA,MACV,IAAA,EAAM,QAAA;AAAA,MACN,UAAA,EAAY;AAAA,QACV,CAAA,EAAG;AAAA,UACD,IAAA,EAAM,OAAA;AAAA,UACN,KAAA,EAAO;AAAA,YACL,IAAA,EAAM,QAAA;AAAA,YACN,UAAA,EAAY;AAAA,cACV,EAAA,EAAI,EAAE,IAAA,EAAM,SAAA,EAAW,aAAa,mBAAA,EAAiB;AAAA,cACrD,IAAA,EAAM,EAAE,IAAA,EAAM,QAAA,EAAU,IAAA,EAAM,CAAC,QAAA,EAAU,SAAA,EAAW,YAAA,EAAc,aAAA,EAAe,SAAA,EAAW,SAAS,CAAA,EAAE;AAAA,cACvG,CAAA,EAAG,EAAE,IAAA,EAAM,CAAC,WAAW,MAAM,CAAA,EAAG,aAAa,qCAAA,EAAmC;AAAA,cAChF,IAAA,EAAM,EAAE,IAAA,EAAM,CAAC,UAAU,MAAM,CAAA,EAAG,aAAa,yBAAA,EAA0B;AAAA,cACzE,CAAA,EAAG,EAAE,IAAA,EAAM,CAAC,WAAW,MAAM,CAAA,EAAG,aAAa,oBAAA,EAAkB;AAAA,cAC/D,CAAA,EAAG,EAAE,IAAA,EAAM,QAAA,EAAU,aAAa,uBAAA,EAAqB;AAAA,cACvD,IAAA,EAAM,EAAE,IAAA,EAAM,QAAA,EAAU,aAAa,OAAA;AAAQ,aAC/C;AAAA,YACA,QAAA,EAAU,CAAC,IAAA,EAAM,MAAA,EAAQ,KAAK,MAAM;AAAA;AACtC,SACF;AAAA,QACA,MAAA,EAAQ,EAAE,IAAA,EAAM,OAAA,EAAS,OAAO,EAAE,IAAA,EAAM,UAAS;AAAE,OACrD;AAAA,MACA,QAAA,EAAU,CAAC,GAAG;AAAA;AAChB;AAEJ;AAEA,IAAM,GAAA,GAAM,CAAC,CAAA,EAAY,QAAA,GAAW,CAAA,KAAc;AAChD,EAAA,MAAM,CAAA,GAAI,OAAO,CAAA,KAAM,QAAA,GAAW,MAAA,CAAO,CAAA,CAAE,OAAA,CAAQ,GAAA,EAAK,GAAG,CAAC,CAAA,GAAI,MAAA,CAAO,CAAC,CAAA;AACxE,EAAA,OAAO,MAAA,CAAO,QAAA,CAAS,CAAC,CAAA,GAAI,CAAA,GAAI,QAAA;AAClC,CAAA;AAEA,IAAM,GAAA,GAAM,CAAC,CAAA,KAA8B;AACzC,EAAA,MAAM,IAAI,OAAO,CAAA,KAAM,QAAA,GAAW,CAAA,CAAE,MAAK,GAAI,EAAA;AAC7C,EAAA,OAAO,IAAI,CAAA,GAAI,IAAA;AACjB,CAAA;AAGA,IAAM,GAAA,GAAM,CAAC,CAAA,EAAY,MAAA,KAAkC;AACzD,EAAA,MAAM,IAAI,OAAO,CAAA,KAAM,QAAA,GAAW,CAAA,GAAI,OAAO,CAAC,CAAA;AAC9C,EAAA,OAAO,MAAA,CAAO,UAAU,CAAC,CAAA,IAAK,KAAK,CAAA,IAAK,CAAA,IAAK,SAAS,CAAA,GAAI,IAAA;AAC5D,CAAA;AAQO,SAAS,gBAAA,CACd,WACA,KAAA,EACe;AACf,EAAA,MAAM,IAAA,GAAO,IAAA,CAAK,SAAA,EAAW,IAAI,CAAA;AACjC,EAAA,IAAI,CAAC,MAAM,OAAO,IAAA;AAClB,EAAA,OAAO,KAAA,CAAM,IAAA,CAAK,CAAC,CAAA,KAAM,KAAK,CAAA,CAAE,YAAY,CAAA,KAAM,IAAA,IAAQ,KAAK,CAAA,CAAE,IAAI,CAAA,KAAM,IAAI,GAAG,EAAA,IAAM,IAAA;AAC1F;AAGO,SAAS,sBAAA,CAAuB,MAA0B,IAAA,EAAoC;AACnG,EAAA,OAAO,KAAK,SAAA,CAAU;AAAA,IACpB,IAAA,EAAM,EAAE,CAAA,EAAG,IAAA,CAAK,MAAA,EAAQ,MAAM,IAAA,CAAK,YAAA,EAAc,GAAA,EAAK,IAAA,CAAK,eAAA,EAAgB;AAAA;AAAA;AAAA,IAG3E,MAAA,EAAQ,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,CAAC,CAAA,EAAG,CAAA,MAAO,EAAE,CAAA,EAAG,CAAA,GAAI,CAAA,EAAG,CAAA,EAAG,CAAA,CAAE,MAAK,CAAE,CAAA;AAAA,IAC7D,OAAO,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,CAAC,MAAM,CAAA,KAAM;AACjC,MAAA,MAAM,GAAA,GAAM,cAAA,CAAe,IAAA,CAAK,IAAI,CAAA;AACpC,MAAA,OAAO;AAAA,QACL,IAAI,CAAA,GAAI,CAAA;AAAA,QACR,GAAG,IAAA,CAAK,WAAA;AAAA,QACR,IAAI,IAAA,CAAK,IAAA;AAAA,QACT,GAAG,IAAA,CAAK,QAAA;AAAA,QACR,IAAI,IAAA,CAAK,SAAA;AAAA,QACT,GAAG,IAAA,CAAK,KAAA;AAAA,QACR,GAAI,KAAK,GAAA,GAAM,EAAE,KAAK,IAAA,CAAK,GAAA,KAAQ,EAAC;AAAA,QACpC,GAAI,GAAA,GAAM,EAAE,GAAA,KAAQ,EAAC;AAAA;AAAA;AAAA;AAAA,QAIrB,GAAI,IAAA,CAAK,WAAA,IAAe,KAAK,IAAA,CAAK,WAAW,MAAM,IAAA,CAAK,IAAA,CAAK,IAAI,CAAA,GAC7D,EAAE,OAAO,IAAA,CAAK,WAAA,EAAa,OAAO,IAAA,CAAK,eAAA,KACvC,EAAC;AAAA,QACL,IAAI,IAAA,CAAK,UAAA,CAAW,CAAC,CAAA,IAAK,IAAI,GAAA,CAAI,CAAC,IAAA,EAAM,CAAA,MAAO,EAAE,CAAA,EAAG,CAAA,GAAI,GAAG,CAAA,EAAG,IAAA,CAAK,MAAK,CAAE;AAAA,OAC7E;AAAA,IACF,CAAC;AAAA,GACF,CAAA;AACH;AAGO,IAAM,cAAA,GAAiB,CAAC,IAAA,EAA0B,IAAA,KACvD,kBAAkB,MAAA,GAAS,sBAAA,CAAuB,IAAA,EAAM,IAAI,CAAA,CAAE,MAAA;AAazD,SAAS,UAAA,CACd,KAAA,EAOA,MAAA,GAAiB,cAAA,EACK;AACtB,EAAA,MAAM,MAAA,GAAS,CAAC,KAAA,MAA4C;AAAA,IAC1D,KAAA;AAAA,IACA,UAAA,EAAY,KAAA,CAAM,GAAA,CAAI,CAAC,CAAA,KAAM,KAAA,CAAM,UAAA,CAAW,CAAA,CAAE,EAAE,CAAA,IAAK,EAAE,CAAA;AAAA,IACzD,UAAU,KAAA,CAAM,QAAA;AAAA,IAChB,OAAO,KAAA,CAAM;AAAA,GACf,CAAA;AAEA,EAAA,MAAM,QAA8B,EAAC;AACrC,EAAA,MAAM,IAAA,GAAsB,CAAC,KAAA,CAAM,KAAK,CAAA;AACxC,EAAA,OAAO,KAAK,MAAA,EAAQ;AAClB,IAAA,MAAM,KAAA,GAAQ,KAAK,KAAA,EAAM;AACzB,IAAA,IAAI,KAAA,CAAM,WAAW,CAAA,EAAG;AACxB,IAAA,MAAM,IAAA,GAAO,OAAO,KAAK,CAAA;AACzB,IAAA,IAAI,KAAA,CAAM,WAAW,CAAA,IAAK,cAAA,CAAe,MAAM,KAAA,CAAM,IAAI,KAAK,MAAA,EAAQ;AACpE,MAAA,KAAA,CAAM,KAAK,IAAI,CAAA;AACf,MAAA;AAAA,IACF;AACA,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,IAAA,CAAK,KAAA,CAAM,SAAS,CAAC,CAAA;AAGvC,IAAA,IAAA,CAAK,OAAA,CAAQ,MAAM,KAAA,CAAM,CAAA,EAAG,IAAI,CAAA,EAAG,KAAA,CAAM,KAAA,CAAM,IAAI,CAAC,CAAA;AAAA,EACtD;AACA,EAAA,OAAO,KAAA;AACT;AAGA,SAAS,WAAW,IAAA,EAAsB;AACxC,EAAA,MAAM,OAAA,GAAU,KAAK,IAAA,EAAK;AAC1B,EAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,KAAA,CAAM,8BAA8B,CAAA;AAC3D,EAAA,IAAI,MAAA,EAAQ,OAAO,MAAA,CAAO,CAAC,EAAE,IAAA,EAAK;AAClC,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,OAAA,CAAQ,GAAG,CAAA;AACjC,EAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,WAAA,CAAY,GAAG,CAAA;AACnC,EAAA,OAAO,KAAA,IAAS,KAAK,GAAA,GAAM,KAAA,GAAQ,QAAQ,KAAA,CAAM,KAAA,EAAO,GAAA,GAAM,CAAC,CAAA,GAAI,OAAA;AACrE;AAaO,SAAS,wBAAA,CACd,OACA,IAAA,EACsB;AACtB,EAAA,IAAI,GAAA;AACJ,EAAA,IAAI,OAAO,UAAU,QAAA,EAAU;AAC7B,IAAA,IAAI;AACF,MAAA,GAAA,GAAM,IAAA,CAAK,KAAA,CAAM,UAAA,CAAW,KAAK,CAAC,CAAA;AAAA,IACpC,CAAA,CAAA,MAAQ;AACN,MAAA,OAAO,EAAE,KAAA,EAAO,IAAI,QAAA,EAAU,CAAC,uDAAiD,CAAA,EAAE;AAAA,IACpF;AAAA,EACF,CAAA,MAAA,IAAW,KAAA,IAAS,OAAO,KAAA,KAAU,QAAA,EAAU;AAC7C,IAAA,GAAA,GAAM,KAAA;AAAA,EACR,CAAA,MAAO;AACL,IAAA,OAAO,EAAE,KAAA,EAAO,IAAI,QAAA,EAAU,CAAC,6BAA0B,CAAA,EAAE;AAAA,EAC7D;AAEA,EAAA,MAAM,MAAA,GAAS,MAAM,OAAA,CAAQ,GAAA,CAAI,CAAC,CAAA,GAAK,GAAA,CAAI,IAAkB,EAAC;AAC9D,EAAA,MAAM,QAA8B,EAAC;AAErC,EAAA,KAAA,MAAW,SAAS,MAAA,EAAQ;AAC1B,IAAA,MAAM,GAAA,GAAO,SAAS,EAAC;AACvB,IAAA,MAAM,QAAQ,GAAA,CAAI,GAAA,CAAI,EAAA,EAAI,IAAA,CAAK,MAAM,MAAM,CAAA;AAC3C,IAAA,IAAI,UAAU,IAAA,EAAM;AACpB,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,KAAA,GAAQ,CAAC,CAAA;AACjC,IAAA,MAAM,aAAa,IAAA,CAAK,UAAA,CAAW,KAAA,GAAQ,CAAC,KAAK,EAAC;AAElD,IAAA,MAAM,KAAA,GAAQ,GAAA,CAAI,GAAA,CAAI,CAAA,EAAG,WAAW,MAAM,CAAA;AAC1C,IAAA,MAAM,YAAY,KAAA,KAAU,IAAA,GAAO,MAAA,GAAY,UAAA,CAAW,QAAQ,CAAC,CAAA;AACnE,IAAA,MAAM,SAAS,GAAA,CAAI,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,SAAS,MAAM,CAAA;AAC9C,IAAA,MAAM,IAAA,GAAO,GAAA,CAAI,GAAA,CAAI,IAAI,CAAA;AACzB,IAAA,MAAM,KAAA,GAAQ,GAAA,CAAI,GAAA,CAAI,CAAA,EAAG,CAAC,CAAA;AAE1B,IAAA,KAAA,CAAM,IAAA,CAAK;AAAA,MACT,QAAQ,IAAA,CAAK,EAAA;AAAA,MACb,SAAA,EAAW,WAAA,CAAY,QAAA,CAAS,IAAsB,IAAK,IAAA,GAA0B,IAAA;AAAA,MACrF,SAAA,EAAW,WAAW,EAAA,IAAM,IAAA;AAAA;AAAA;AAAA;AAAA,MAI5B,cAAA,EAAgB,YAAY,IAAA,GAAQ,GAAA,CAAI,IAAI,IAAI,CAAA,IAAK,GAAA,CAAI,IAAA,CAAK,WAAW,CAAA;AAAA,MACzE,WAAA,EAAa,gBAAA,CAAiB,SAAA,EAAW,IAAA,CAAK,KAAK,CAAA;AAAA;AAAA,MAEnD,gBAAA,EAAkB,KAAA,GAAQ,CAAA,GAAI,KAAA,GAAQ,CAAA;AAAA,MACtC,cAAA,EAAgB,WAAW,IAAA,GAAO,IAAA,GAAO,KAAK,QAAA,CAAS,MAAA,GAAS,CAAC,CAAA,CAAE,EAAA;AAAA,MACnE,UAAA,EAAY,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,GAAA,CAAI,GAAA,CAAI,IAAA,EAAM,CAAC,CAAC,CAAC;AAAA,KACtD,CAAA;AAAA,EACH;AAEA,EAAA,MAAM,QAAA,GAAW,MAAM,OAAA,CAAQ,GAAA,CAAI,MAAM,CAAA,GACpC,GAAA,CAAI,MAAA,CAAqB,GAAA,CAAI,CAAC,CAAA,KAAM,IAAI,CAAC,CAAC,EAAE,MAAA,CAAO,CAAC,MAAmB,CAAC,CAAC,CAAC,CAAA,GAC3E,EAAC;AAEL,EAAA,OAAO,EAAE,OAAO,QAAA,EAAS;AAC3B","file":"chunk-5LI2FDOM.js","sourcesContent":["import type {\n ChartAccountOption, InboxEntryType, InboxItem,\n MeasurementUnitOption,\n} from '../types'\n\n// ---------------------------------------------------------------------------\n// «Sugerir com IA» — a lógica pura, compartilhada com a edge function.\n// ---------------------------------------------------------------------------\n// É o porte do `danfe-ai-suggest` do V1, e roda no MESMO lugar que lá: uma edge\n// function (`functions/fiscalbr-suggest-note-items`). A primeira tentativa foi\n// pelo broker do agente e ele recusou no primeiro clique real:\n//\n// 400 {\"status\":\"error\",\"message\":\"Message is too long\"}\n//\n// O limite não era o erro, era o sintoma. Um turno de chat é o lugar errado para\n// isto pelas mesmas quatro razões que o `scribe-generate` já tinha escrito: teto\n// de rodadas, morre na navegação, roda no plano do cliente, e persiste tudo no\n// histórico de conversa de alguém.\n//\n// Este arquivo é PURO e sem dependência de rede: a function importa daqui para\n// que o prompt tenha UMA definição só, e os testes provam o parser sem subir\n// nada. Duas cópias divergindo é como a sugestão do servidor deixa de bater com\n// o que a tela prometeu.\n// ---------------------------------------------------------------------------\n\n/** Os cinco tipos do V1 (3.402 linhas de produção) mais uso e consumo. */\nconst ENTRY_TYPES: readonly InboxEntryType[] = ['insumo', 'revenda', 'patrimonio', 'uso_consumo', 'despesa', 'servico']\n\n// ── Embalagem cujo conteúdo é um NÚMERO, não um palpite ────────────────────\n// CX, FD e SC não entram aqui: «caixa» não diz quantas unidades tem, e só a\n// descrição ou o peso do XML respondem. Estas três dizem — a palavra É a conta.\n//\n// MILHEIRO é a que dói. «1.000 MI» de embalagem lido como fator 1 põe mil peças\n// no estoque como se fossem mil milheiros: erro de 1.000x, silencioso, e o custo\n// unitário sai dividido por mil. Era o caso que o prompt do V1 não cobria.\nconst PACKAGING_COUNT: Record<string, number> = {\n MI: 1000, MIL: 1000, MLH: 1000, MILHEIRO: 1000, 'MI/1000': 1000,\n DZ: 12, DUZIA: 12, 'DÚZIA': 12,\n CENTO: 100, CENT: 100,\n}\n\n/** Unidade de estoque que conta PEÇA. Só nela o conteúdo da embalagem é o fator. */\nconst COUNTING_UNITS = new Set(['UN', 'UND', 'UNID', 'UNIDADE', 'PC', 'PÇ', 'PCS', 'PEC', 'PEÇA', 'PECA'])\n\nconst norm = (u: string | null | undefined) =>\n (u ?? '').trim().toUpperCase().replace(/\\.$/, '')\n\n/** Quantas peças a unidade comercial declara, quando ela declara. */\nexport function packagingCount(commercialUnit: string | null | undefined): number | null {\n return PACKAGING_COUNT[norm(commercialUnit)] ?? null\n}\n\nexport const isCountingUnit = (abbreviation: string | null | undefined) =>\n COUNTING_UNITS.has(norm(abbreviation))\n\n/**\n * O FATOR QUE O XML JÁ DIZ — a evidência mais forte, e é uma divisão.\n *\n * `uTrib`/`qTrib` são a unidade e a quantidade TRIBUTÁVEIS da NF-e: o fornecedor\n * vende em CAIXA mas declara ao fisco em KG. Quando a unidade tributável é a\n * unidade em que a casa guarda, o fator não é palpite nenhum:\n *\n * 4 CX com qTrib 88,224 KG → fator = 88,224 ÷ 4 = 22,056\n *\n * É a regra nº 1 do prompt do V1, e era a que faltava virar conta: o caso\n * Artorius (carne a R$ 397/kg em vez de ~R$ 18) foi esta divisão não ter sido\n * feita. Devolve `null` quando a unidade tributável é outra coisa — aí só a\n * descrição responde, e isso é julgamento, não aritmética.\n */\nexport function taxableFactor(\n item: Pick<InboxItem, 'unit' | 'quantity' | 'unitTaxable' | 'quantityTaxable'>,\n stockUnitAbbreviation: string | null | undefined,\n): number | null {\n const trib = norm(item.unitTaxable)\n const estoque = norm(stockUnitAbbreviation)\n if (!trib || !estoque || trib !== estoque) return null\n // Mesma unidade comercial e tributável é fator 1, e não há o que calcular.\n if (trib === norm(item.unit)) return null\n if (!item.quantityTaxable || item.quantity <= 0) return null\n const fator = item.quantityTaxable / item.quantity\n return fator > 0 ? Math.round(fator * 10000) / 10000 : null\n}\n\n/**\n * O FATOR QUE OS DADOS JÁ DIZEM, aplicado depois da resposta do modelo.\n *\n * Prompt é pedido, não garantia. Duas evidências aqui são aritmética e por isso\n * não se delegam:\n *\n * ① `uTrib`/`qTrib` batendo com a unidade de estoque → uma divisão (ver\n * `taxableFactor`). Ganha de tudo, inclusive do que o modelo respondeu.\n * ② embalagem que CONTA peça (MI, DZ, CENTO) → o número está na sigla. Só\n * entra quando o modelo não calculou nada (fator 1).\n *\n * ENTRE 1 E 1.000, O PADRÃO É 1.000. «Milheiro» significa mil peças: é o que a\n * palavra diz, e discordar dela exige evidência. A primeira versão disto era\n * conservadora no NÚMERO — mantinha 1 quando não dava para confirmar a unidade de\n * estoque — e conservadorismo no número é o pior lugar para ele:\n *\n * 1 MI a R$ 616,00 fator 1 → 1 saco a R$ 616,00 cada (absurdo)\n * fator 1000 → 1.000 sacos a R$ 0,616 (certo)\n *\n * Errar para 1 não é cauteloso, é 1.000x errado em silêncio. O cuidado mora no\n * PORTÃO: a tela não deixa aceitar enquanto alguém não confirmar a unidade de\n * estoque, e a confiança baixa marca a linha como «confira».\n *\n * A exceção continua sendo o estoque em PESO: ninguém sabe quanto pesa o\n * milheiro, e ali 1.000 seria um chute de outra natureza. Aí o fator fica como\n * está e sai aviso.\n */\nexport function enforceFactorEvidence(\n result: NoteSuggestionResult,\n items: Array<Pick<InboxItem, 'id' | 'unit' | 'description' | 'quantity' | 'unitTaxable' | 'quantityTaxable'>>,\n units: Array<Pick<MeasurementUnitOption, 'id' | 'abbreviation'>>,\n): NoteSuggestionResult {\n const avisos = [...result.warnings]\n const byId = new Map(items.map((i) => [i.id, i]))\n const unitAbbr = new Map(units.map((u) => [u.id, u.abbreviation]))\n\n const corrigidos = result.items.map((s) => {\n const item = byId.get(s.itemId)\n if (!item) return s\n const sigla = s.stockUnitId ? unitAbbr.get(s.stockUnitId) : null\n\n // ① O XML tem a conta pronta.\n const doXml = taxableFactor(item, sigla)\n if (doXml !== null) return { ...s, conversionFactor: doXml }\n\n // ② A sigla da embalagem tem o número.\n const conteudo = packagingCount(item.unit)\n if (!conteudo || s.conversionFactor !== 1) return s\n\n // Unidade de estoque que conta peça: a conta é exata, e a confiança fica.\n if (isCountingUnit(sigla)) return { ...s, conversionFactor: conteudo }\n\n // Unidade de estoque DESCONHECIDA — produto sem `unit` no cadastro, que é o\n // caso comum. Aplica o conteúdo mesmo assim, porque é o que a palavra diz, e\n // baixa a confiança: a tela pede a unidade antes de deixar aceitar.\n if (!sigla) return { ...s, conversionFactor: conteudo, confidence: Math.min(s.confidence, 0.5) }\n\n // Estoque em PESO: quanto pesa um milheiro é outra pergunta, e 1.000 seria um\n // chute de natureza diferente. O fator fica como está e o aviso sai.\n avisos.push(\n `«${item.description}» vem em ${norm(item.unit)} (${conteudo} por embalagem) `\n + `e o estoque é em ${norm(sigla)}: confira o fator de conversão.`,\n )\n return { ...s, confidence: Math.min(s.confidence, 0.3) }\n })\n\n return { items: corrigidos, warnings: avisos }\n}\n\n// ---------------------------------------------------------------------------\n// A PERGUNTA, E A TRADUÇÃO DE VOLTA.\n// ---------------------------------------------------------------------------\n// As otimizações da tentativa anterior FICAM, porque tamanho continua custando\n// dinheiro no provedor do modelo — e duas delas não são sobre tamanho. A conta\n// de um tenant de verdade, no formato do V1:\n//\n// 251 unidades de medida × ~75 chars (uuid + nome + sigla) ≈ 19.000\n// 149 contas folha × ~80 chars (uuid + código + nome) ≈ 12.000\n// 25 itens × 8 candidatos × ~70 chars (uuid + nome) ≈ 15.000\n// prompt ≈ 2.600\n// ─────────\n// 48.600\n//\n// ① O MODELO NÃO VÊ UUID. Tudo numerado — item 1, candidato 2, conta 37 — e o\n// servidor traduz de volta. Corta ~60% do payload E mata a alucinação de id\n// na raiz: não há como inventar um identificador que não seja um número\n// dentro da faixa recebida. O `pick()` do V1, que filtrava id de outra rede,\n// deixou de ser filtro e passou a ser verificação de limite.\n//\n// ② A UNIDADE DE ESTOQUE SAIU DA PERGUNTA. Não era julgamento: a unidade de\n// estoque é a do PRODUTO escolhido, e quem escolheu sabe qual é. Deixar o\n// modelo escolher entre 251 unidades eram 19.000 caracteres por uma resposta\n// que, quando diferia da unidade do produto, estava errada.\n//\n// ③ CORTA-SE POR TAMANHO, não por contagem de itens. «25 itens» não é medida:\n// 25 itens sem candidato são 3 KB, 25 com oito candidatos cada são 20 KB.\n// ---------------------------------------------------------------------------\n\nexport interface NoteItemSuggestion {\n itemId: string\n entryType: InboxEntryType | null\n productId: string | null\n /** O nome que o modelo propõe quando nada casa. A tela ainda não cria produto. */\n newProductName: string | null\n stockUnitId: string | null\n conversionFactor: number\n chartAccountId: string | null\n /** 0..1. Abaixo de 0,6 a tela marca a linha como «confira». */\n confidence: number\n}\n\nexport interface NoteSuggestionResult {\n items: NoteItemSuggestion[]\n warnings: string[]\n}\n\n/** Um produto que o índice ofereceu para UMA linha da nota (migration 012). */\nexport interface SuggestionCandidate {\n id: string\n name: string\n sku?: string | null\n /** A unidade do PRODUTO. É ela que vira unidade de estoque — ver `resolveStockUnit`. */\n unit?: string | null\n}\n\n/** A tradução de volta: o que cada número daquele lote significa. */\nexport interface SuggestionCodebook {\n items: InboxItem[]\n /** Candidatos na ordem em que foram numerados, por item (mesmo índice de `items`). */\n candidates: SuggestionCandidate[][]\n accounts: ChartAccountOption[]\n units: MeasurementUnitOption[]\n}\n\nexport interface SuggestionNoteHeader {\n number: string\n supplierName: string\n natureOperation: string | null\n}\n\n/**\n * Teto da mensagem por lote, em caracteres.\n *\n * Folgado porque agora o teto é o do MODELO e não o de um broker de chat: a\n * function fala direto com o provedor. 24 KB de pergunta cabem com sobra na\n * janela e ainda deixam a resposta inteira voltar.\n */\nexport const MESSAGE_BUDGET = 24000\n\nexport const SUGGESTION_PROMPT = [\n 'Você associa itens de uma NF-e de compra aos produtos cadastrados da empresa.',\n 'Responda SEMPRE pela tool `aplicar_sugestoes`. Tudo é NUMERADO: use só números que',\n 'apareceram nos dados. `c` é o número de um candidato DAQUELE item.',\n 'Sem candidato bom (semelhança abaixo de 0,6) ou lista vazia: c=null e `novo` com o nome.',\n '',\n 'f — fator de conversão (unidade comercial da nota × unidade de estoque):',\n '- EVIDÊNCIA MAIS FORTE: `utrib`/`qtrib` no item são a unidade e a quantidade tributáveis do',\n ' XML. Se `utrib` for a unidade em que o produto é guardado, f = qtrib ÷ q (4 CX com qtrib',\n ' 88,224 KG → f = 22,056). Quando isso vale, não há o que julgar.',\n '- Embalagem (CX, FD, SC, CJ, PC, PCT, EMB, DZ, MI) com estoque em peso ou peça: f NUNCA é 1.',\n ' Descubra quanto cada embalagem contém pela descrição (\"CG 5,5/8,5KG\", \"BALDE 3KG\" → 3).',\n '- `emb` no item é o conteúdo que a própria sigla declara e não se discute: MI (milheiro)',\n ' são 1.000, DZ são 12, CENTO são 100. Com estoque em peça, f é exatamente esse número.',\n '- Confira o custo: custo por kg = t ÷ (q × f). Absurdo para o tipo de produto (carne a',\n ' R$ 397/kg) significa f errado.',\n '- Sem evidência de peso: f=1 e conf no máximo 0,4 — melhor revisar que entrar errado.',\n '',\n 'a — plano de contas:',\n '- A conta MAIS ESPECÍFICA cujo nome represente a natureza do item.',\n '- NUNCA a mesma conta para todos: cada item é avaliado sozinho.',\n '- Limpeza/papel toalha → \"Material de limpeza\"; secos → \"Mercearia\"; bebidas → \"Bebidas\";',\n ' embalagem → \"Embalagens\"; equipamento → \"Imobilizado\". Sem conta razoável: a=null.',\n '',\n 'tipo: uso_consumo é o que a casa gasta sem vender nem transformar (luva, descartável,',\n ' limpeza) e ENTRA no estoque. despesa e servico NÃO viram estoque. Só para frete, serviço, taxa.',\n 'avisos: o que ficou sem evidência. Vazio quando não houve nenhum.',\n].join('\\n')\n\n/**\n * O schema da tool. O V1 forçava `tool_choice`, e é isso que mantém a resposta\n * sendo dado em vez de prosa — texto livre voltava «Claro! Vou analisar os\n * itens…» e o parser ficava sem nada para ler.\n */\nexport const SUGGESTION_TOOL = {\n type: 'function',\n function: {\n name: 'aplicar_sugestoes',\n description: 'Associações sugeridas para cada item numerado da NF-e',\n parameters: {\n type: 'object',\n properties: {\n r: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n it: { type: 'integer', description: 'número do item' },\n tipo: { type: 'string', enum: ['insumo', 'revenda', 'patrimonio', 'uso_consumo', 'despesa', 'servico'] },\n c: { type: ['integer', 'null'], description: 'número do candidato daquele item' },\n novo: { type: ['string', 'null'], description: 'nome do produto a criar' },\n a: { type: ['integer', 'null'], description: 'número da conta' },\n f: { type: 'number', description: 'fator de conversão' },\n conf: { type: 'number', description: '0 a 1' },\n },\n required: ['it', 'tipo', 'f', 'conf'],\n },\n },\n avisos: { type: 'array', items: { type: 'string' } },\n },\n required: ['r'],\n },\n },\n} as const\n\nconst num = (v: unknown, fallback = 0): number => {\n const n = typeof v === 'string' ? Number(v.replace(',', '.')) : Number(v)\n return Number.isFinite(n) ? n : fallback\n}\n\nconst str = (v: unknown): string | null => {\n const s = typeof v === 'string' ? v.trim() : ''\n return s ? s : null\n}\n\n/** Um índice 1..N vindo do modelo, ou `null` se não for isso. */\nconst idx = (v: unknown, length: number): number | null => {\n const n = typeof v === 'number' ? v : Number(v)\n return Number.isInteger(n) && n >= 1 && n <= length ? n : null\n}\n\n/**\n * A UNIDADE DE ESTOQUE É A DO PRODUTO, resolvida aqui e não pelo modelo.\n *\n * Quando o produto não declara unidade, fica nulo — e nulo é honesto, porque o\n * campo continua na tela esperando alguém.\n */\nexport function resolveStockUnit(\n candidate: SuggestionCandidate | undefined,\n units: MeasurementUnitOption[],\n): string | null {\n const alvo = norm(candidate?.unit)\n if (!alvo) return null\n return units.find((u) => norm(u.abbreviation) === alvo || norm(u.name) === alvo)?.id ?? null\n}\n\n/** O corpo da pergunta de UM lote, com tudo numerado. */\nexport function buildSuggestionPayload(book: SuggestionCodebook, note: SuggestionNoteHeader): string {\n return JSON.stringify({\n nota: { n: note.number, forn: note.supplierName, nat: note.natureOperation },\n // `a` é o número da conta; `n`, o nome. Código fiscal fora: o modelo escolhe\n // pelo nome, e o código eram 8 caracteres por linha sem efeito na escolha.\n contas: book.accounts.map((a, i) => ({ a: i + 1, n: a.name })),\n itens: book.items.map((item, i) => {\n const emb = packagingCount(item.unit)\n return {\n it: i + 1,\n d: item.description,\n un: item.unit,\n q: item.quantity,\n vu: item.unitPrice,\n t: item.total,\n ...(item.ncm ? { ncm: item.ncm } : {}),\n ...(emb ? { emb } : {}),\n // A evidência de peso que o V1 lia do XML com regex e a V2 já tem em\n // coluna. Só viaja quando diz algo novo: unidade tributável igual à\n // comercial é fator 1, e repeti-la seria ruído em 3.100 das 3.675 linhas.\n ...(item.unitTaxable && norm(item.unitTaxable) !== norm(item.unit)\n ? { utrib: item.unitTaxable, qtrib: item.quantityTaxable }\n : {}),\n c: (book.candidates[i] ?? []).map((cand, j) => ({ c: j + 1, n: cand.name })),\n }\n }),\n })\n}\n\n/** Quantos caracteres a pergunta deste lote tem. */\nexport const suggestionSize = (book: SuggestionCodebook, note: SuggestionNoteHeader) =>\n SUGGESTION_PROMPT.length + buildSuggestionPayload(book, note).length\n\n/**\n * Divide os itens em lotes que CABEM, medindo cada um.\n *\n * Divisão binária e não «de N em N»: o custo de um item varia de 60 a 700\n * caracteres conforme quantos candidatos o índice achou, então qualquer número\n * fixo erra numa das duas direções — manda mensagem grande demais, ou multiplica\n * as voltas sem precisar.\n *\n * Um item que sozinho não cabe volta como lote de um: cortá-lo em silêncio faria\n * a resposta dizer «preenchi» sobre uma linha intocada.\n */\nexport function splitToFit(\n input: {\n items: InboxItem[]\n candidates: Record<string, SuggestionCandidate[]>\n accounts: ChartAccountOption[]\n units: MeasurementUnitOption[]\n note: SuggestionNoteHeader\n },\n budget: number = MESSAGE_BUDGET,\n): SuggestionCodebook[] {\n const bookOf = (items: InboxItem[]): SuggestionCodebook => ({\n items,\n candidates: items.map((i) => input.candidates[i.id] ?? []),\n accounts: input.accounts,\n units: input.units,\n })\n\n const lotes: SuggestionCodebook[] = []\n const fila: InboxItem[][] = [input.items]\n while (fila.length) {\n const atual = fila.shift()!\n if (atual.length === 0) continue\n const book = bookOf(atual)\n if (atual.length === 1 || suggestionSize(book, input.note) <= budget) {\n lotes.push(book)\n continue\n }\n const meio = Math.ceil(atual.length / 2)\n // `unshift` na ordem: a nota é lida de cima para baixo e a sugestão deve\n // chegar na mesma ordem, senão o item 9 preenche antes do 2.\n fila.unshift(atual.slice(0, meio), atual.slice(meio))\n }\n return lotes\n}\n\n/** Tira o que sobra quando um modelo devolve JSON embrulhado em cerca. */\nfunction stripFence(text: string): string {\n const trimmed = text.trim()\n const fenced = trimmed.match(/```(?:json)?\\s*([\\s\\S]*?)```/)\n if (fenced) return fenced[1].trim()\n const start = trimmed.indexOf('{')\n const end = trimmed.lastIndexOf('}')\n return start >= 0 && end > start ? trimmed.slice(start, end + 1) : trimmed\n}\n\n/**\n * O que o modelo respondeu, virado em sugestão com que a tela sabe lidar.\n *\n * Aceita o objeto já pronto (argumentos da tool) ou o texto cru: a tool é\n * forçada, mas um modelo trocado amanhã pode responder em prosa, e perder a\n * sugestão inteira por uma cerca de código seria barato de evitar.\n *\n * NÃO HÁ ALLOW-LIST DE ID porque não há id: número fora da faixa cai por\n * aritmética. Exportado porque é onde os defeitos aparecem, e isso se prova sem\n * rede.\n */\nexport function parseNoteSuggestionReply(\n reply: unknown,\n book: SuggestionCodebook,\n): NoteSuggestionResult {\n let raw: Record<string, unknown>\n if (typeof reply === 'string') {\n try {\n raw = JSON.parse(stripFence(reply)) as Record<string, unknown>\n } catch {\n return { items: [], warnings: ['Não consegui entender o que voltou da sugestão.'] }\n }\n } else if (reply && typeof reply === 'object') {\n raw = reply as Record<string, unknown>\n } else {\n return { items: [], warnings: ['A sugestão voltou vazia.'] }\n }\n\n const linhas = Array.isArray(raw.r) ? (raw.r as unknown[]) : []\n const items: NoteItemSuggestion[] = []\n\n for (const linha of linhas) {\n const row = (linha ?? {}) as Record<string, unknown>\n const nItem = idx(row.it, book.items.length)\n if (nItem === null) continue\n const item = book.items[nItem - 1]\n const candidatos = book.candidates[nItem - 1] ?? []\n\n const nCand = idx(row.c, candidatos.length)\n const escolhido = nCand === null ? undefined : candidatos[nCand - 1]\n const nConta = idx(row.a, book.accounts.length)\n const tipo = str(row.tipo)\n const fator = num(row.f, 1)\n\n items.push({\n itemId: item.id,\n entryType: ENTRY_TYPES.includes(tipo as InboxEntryType) ? (tipo as InboxEntryType) : null,\n productId: escolhido?.id ?? null,\n // O prompt pede `novo` quando nada casa, e o modelo às vezes devolve null —\n // aí a tela ficava sem produto E sem nome para oferecer. A descrição do\n // item é o padrão honesto, e é o que o V1 gravava em `ai_product_name`.\n newProductName: escolhido ? null : (str(row.novo) ?? str(item.description)),\n stockUnitId: resolveStockUnit(escolhido, book.units),\n // Fator zero ou negativo divide o custo unitário por zero: 1 é honesto.\n conversionFactor: fator > 0 ? fator : 1,\n chartAccountId: nConta === null ? null : book.accounts[nConta - 1].id,\n confidence: Math.min(1, Math.max(0, num(row.conf, 0))),\n })\n }\n\n const warnings = Array.isArray(raw.avisos)\n ? (raw.avisos as unknown[]).map((w) => str(w)).filter((w): w is string => !!w)\n : []\n\n return { items, warnings }\n}\n"]}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { packagingCount } from './chunk-5LI2FDOM.js';
|
|
1
2
|
import { recordRailFields, RECORD_GROUP, activeFacet, describeError, getSupabaseClientOptional, formatCurrency, connectionSettings, connectionStatus, definePlugin, createConnectionStore, settingsStatus, maskCpfCnpj, getActiveTenantId as getActiveTenantId$1 } from '@fayz-ai/core';
|
|
2
3
|
import React6, { useState, useCallback, useEffect, useMemo, useRef } from 'react';
|
|
3
4
|
import { ModalHeader, ModalTitle, ModalBody, Skeleton, AiActionButton, Dropdown, DropdownTrigger, Button, DropdownContent, DropdownItem, Hint, Card, InlineLink, SearchSelect, Input, EmBreve, DatePicker, ModalFooter, SubpageHeader, Modal, ModalContent, ModalDescription, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, useFilterDraft, RowActions, PageHeaderActions, FilterPanel, FilterPanelField, SearchField, TooltipProvider, DataTable, ChevronPager, Tooltip, TooltipTrigger, TooltipContent, Badge, SegmentedControl, ComingSoon, toast, CardHeader, CardTitle, CardContent, CardDescription, Switch } from '@fayz-ai/ui';
|
|
@@ -7,7 +8,6 @@ import { Loader2, Download, ChevronDown, FileCode2, FileText, RefreshCw, UserPlu
|
|
|
7
8
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
8
9
|
import { FINANCIAL_INVOICE_ZONES } from '@fayz-ai/financial/views/zones';
|
|
9
10
|
|
|
10
|
-
// src/index.ts
|
|
11
11
|
var PLUGNOTAS_CONNECTOR_ID = "plugnotas";
|
|
12
12
|
var PLUGNOTAS_MARK = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%230F766E'/%3E%3Cpath d='M10 7h12v18l-2-1.5-2 1.5-2-1.5-2 1.5-2-1.5-2 1.5V7Z' fill='white'/%3E%3Cpath d='M13 12h6M13 16h6M13 20h4' stroke='%230F766E' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E";
|
|
13
13
|
var PLUGNOTAS_FIELDS = [
|
|
@@ -4410,6 +4410,34 @@ BEGIN
|
|
|
4410
4410
|
END
|
|
4411
4411
|
$m$;
|
|
4412
4412
|
`;
|
|
4413
|
+
var MIGRATION_029_THE_SERVICE_WRITES_THE_INTAKE_HEALTH = `-- ============================================================================
|
|
4414
|
+
-- 029_the_service_writes_the_intake_health \u2014 the DF-e pull moves to the
|
|
4415
|
+
-- integration service (ADR 0003 phase 2).
|
|
4416
|
+
--
|
|
4417
|
+
-- The pull reads a tenant's emitter rows AS the caller (RLS decides which), and
|
|
4418
|
+
-- then records each CNPJ's outcome in three health columns, even for a reader
|
|
4419
|
+
-- without settings.manage, so the notes list can say "intake stopped". The
|
|
4420
|
+
-- edge function did that with service_role; the service gets exactly those
|
|
4421
|
+
-- three columns, by row id, and nothing else on the table.
|
|
4422
|
+
--
|
|
4423
|
+
-- Guarded: the role is an operator step. Replay-safe.
|
|
4424
|
+
-- ============================================================================
|
|
4425
|
+
|
|
4426
|
+
DO $$
|
|
4427
|
+
BEGIN
|
|
4428
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'fayz_integration')
|
|
4429
|
+
AND to_regclass('public.plg_fiscalbr_emitter_settings') IS NOT NULL THEN
|
|
4430
|
+
GRANT SELECT (id) ON public.plg_fiscalbr_emitter_settings TO fayz_integration;
|
|
4431
|
+
GRANT UPDATE (dfe_last_sync_at, dfe_last_error, dfe_last_error_at) ON public.plg_fiscalbr_emitter_settings TO fayz_integration;
|
|
4432
|
+
DROP POLICY IF EXISTS plg_fiscalbr_emitter_settings_service_health ON public.plg_fiscalbr_emitter_settings;
|
|
4433
|
+
CREATE POLICY plg_fiscalbr_emitter_settings_service_health ON public.plg_fiscalbr_emitter_settings
|
|
4434
|
+
FOR UPDATE TO fayz_integration USING (true) WITH CHECK (true);
|
|
4435
|
+
DROP POLICY IF EXISTS plg_fiscalbr_emitter_settings_service_read ON public.plg_fiscalbr_emitter_settings;
|
|
4436
|
+
CREATE POLICY plg_fiscalbr_emitter_settings_service_read ON public.plg_fiscalbr_emitter_settings
|
|
4437
|
+
FOR SELECT TO fayz_integration USING (true);
|
|
4438
|
+
END IF;
|
|
4439
|
+
END $$;
|
|
4440
|
+
`;
|
|
4413
4441
|
var MIGRATIONS = [
|
|
4414
4442
|
{ id: "000_baseline", sql: MIGRATION_000_BASELINE },
|
|
4415
4443
|
{ id: "001_preserve_terminal_results_and_recover_leases", sql: MIGRATION_001_PRESERVE_TERMINAL_RESULTS_AND_RECOVER_LEASES },
|
|
@@ -4439,7 +4467,8 @@ var MIGRATIONS = [
|
|
|
4439
4467
|
{ id: "025_a_conta_da_nota_fecha_com_o_total", sql: MIGRATION_025_A_CONTA_DA_NOTA_FECHA_COM_O_TOTAL },
|
|
4440
4468
|
{ id: "026_o_recebimento_parado_diz_por_que", sql: MIGRATION_026_O_RECEBIMENTO_PARADO_DIZ_POR_QUE },
|
|
4441
4469
|
{ id: "027_a_nota_com_conta_e_sem_estoque_termina_no_v2", sql: MIGRATION_027_A_NOTA_COM_CONTA_E_SEM_ESTOQUE_TERMINA_NO_V2 },
|
|
4442
|
-
{ id: "028_a_linha_de_estoque_tem_unidade", sql: MIGRATION_028_A_LINHA_DE_ESTOQUE_TEM_UNIDADE }
|
|
4470
|
+
{ id: "028_a_linha_de_estoque_tem_unidade", sql: MIGRATION_028_A_LINHA_DE_ESTOQUE_TEM_UNIDADE },
|
|
4471
|
+
{ id: "029_the_service_writes_the_intake_health", sql: MIGRATION_029_THE_SERVICE_WRITES_THE_INTAKE_HEALTH }
|
|
4443
4472
|
];
|
|
4444
4473
|
var taxGroupEntity = {
|
|
4445
4474
|
name: "Grupo fiscal",
|
|
@@ -6208,24 +6237,6 @@ function NoteAttachmentsCard({ noteId, invoiceId, readOnly, instalments, onInsta
|
|
|
6208
6237
|
// src/types.ts
|
|
6209
6238
|
var STOCK_BEARING_ENTRY_TYPES = ["insumo", "revenda", "patrimonio", "uso_consumo"];
|
|
6210
6239
|
var isStockBearing = (type) => STOCK_BEARING_ENTRY_TYPES.includes(type);
|
|
6211
|
-
|
|
6212
|
-
// src/lib/note-suggestion.ts
|
|
6213
|
-
var PACKAGING_COUNT = {
|
|
6214
|
-
MI: 1e3,
|
|
6215
|
-
MIL: 1e3,
|
|
6216
|
-
MLH: 1e3,
|
|
6217
|
-
MILHEIRO: 1e3,
|
|
6218
|
-
"MI/1000": 1e3,
|
|
6219
|
-
DZ: 12,
|
|
6220
|
-
DUZIA: 12,
|
|
6221
|
-
"D\xDAZIA": 12,
|
|
6222
|
-
CENTO: 100,
|
|
6223
|
-
CENT: 100
|
|
6224
|
-
};
|
|
6225
|
-
var norm = (u) => (u ?? "").trim().toUpperCase().replace(/\.$/, "");
|
|
6226
|
-
function packagingCount(commercialUnit) {
|
|
6227
|
-
return PACKAGING_COUNT[norm(commercialUnit)] ?? null;
|
|
6228
|
-
}
|
|
6229
6240
|
var money3 = (v) => formatCurrency(v || 0, "BRL");
|
|
6230
6241
|
var qty = (v) => new Intl.NumberFormat("pt-BR", { maximumFractionDigits: 4 }).format(v || 0);
|
|
6231
6242
|
var day3 = (iso) => iso ? iso.slice(0, 10).split("-").reverse().join("/") : "\u2014";
|