@fayz-ai/plugin-financial 0.25.9 → 0.25.10

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,1736 @@
1
+ import * as React from 'react';
2
+ import { revealPage, askAI, requestChatUpload, toInlineFile, useChatStore, createArchetypeLookup, adminNavigateTo, useAgentSurface, resolveEntityRoute } from '@fayz-ai/admin';
3
+ import { round2 } from '@fayz-ai/financial/domain/money';
4
+ import { formatCurrency } from '@fayz-ai/core';
5
+
6
+ // src/views/payable-agent.tsx
7
+
8
+ // src/lib/amount-in-words.ts
9
+ var UNITS = {
10
+ zero: 0,
11
+ um: 1,
12
+ uma: 1,
13
+ dois: 2,
14
+ duas: 2,
15
+ tres: 3,
16
+ quatro: 4,
17
+ cinco: 5,
18
+ seis: 6,
19
+ sete: 7,
20
+ oito: 8,
21
+ nove: 9,
22
+ dez: 10,
23
+ onze: 11,
24
+ doze: 12,
25
+ treze: 13,
26
+ catorze: 14,
27
+ quatorze: 14,
28
+ quinze: 15,
29
+ dezesseis: 16,
30
+ dezasseis: 16,
31
+ dezessete: 17,
32
+ dezassete: 17,
33
+ dezoito: 18,
34
+ dezenove: 19,
35
+ dezanove: 19,
36
+ vinte: 20,
37
+ trinta: 30,
38
+ quarenta: 40,
39
+ cinquenta: 50,
40
+ cincoenta: 50,
41
+ sessenta: 60,
42
+ setenta: 70,
43
+ oitenta: 80,
44
+ noventa: 90,
45
+ cem: 100,
46
+ cento: 100,
47
+ duzentos: 200,
48
+ duzentas: 200,
49
+ trezentos: 300,
50
+ trezentas: 300,
51
+ quatrocentos: 400,
52
+ quatrocentas: 400,
53
+ quinhentos: 500,
54
+ quinhentas: 500,
55
+ seiscentos: 600,
56
+ seiscentas: 600,
57
+ setecentos: 700,
58
+ setecentas: 700,
59
+ oitocentos: 800,
60
+ oitocentas: 800,
61
+ novecentos: 900,
62
+ novecentas: 900
63
+ };
64
+ var SCALES = {
65
+ mil: 1e3,
66
+ milhao: 1e6,
67
+ milhoes: 1e6,
68
+ bilhao: 1e9,
69
+ bilhoes: 1e9
70
+ };
71
+ function normalize(text2) {
72
+ return text2.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/[^a-z\s]/g, " ").split(/\s+/).filter(Boolean);
73
+ }
74
+ function sumGroup(words) {
75
+ let total = 0;
76
+ for (const word of words) {
77
+ const value = UNITS[word];
78
+ if (value !== void 0) total += value;
79
+ }
80
+ return total;
81
+ }
82
+ function parseAmountInWords(raw) {
83
+ if (typeof raw !== "string") return null;
84
+ const words = normalize(raw);
85
+ if (!words.length) return null;
86
+ const centsAt = words.findIndex((w) => w === "centavo" || w === "centavos");
87
+ const whole = centsAt >= 0 ? words.slice(0, centsAt) : words;
88
+ const cents = centsAt >= 0 ? sumGroup(words.slice(0, centsAt).slice(lastCurrencyIndex(words, centsAt))) : 0;
89
+ let total = 0;
90
+ let group = [];
91
+ let sawNumber = false;
92
+ for (const word of whole) {
93
+ if (word === "centavo" || word === "centavos") break;
94
+ const scale = SCALES[word];
95
+ if (scale !== void 0) {
96
+ total += (sumGroup(group) || 1) * scale;
97
+ group = [];
98
+ sawNumber = true;
99
+ continue;
100
+ }
101
+ if (UNITS[word] !== void 0) {
102
+ group.push(word);
103
+ sawNumber = true;
104
+ }
105
+ }
106
+ total += sumGroup(group);
107
+ if (!sawNumber) return null;
108
+ const value = centsAt >= 0 && cents > 0 && cents < 100 ? total - cents + cents / 100 : total;
109
+ return value > 0 ? Math.round(value * 100) / 100 : null;
110
+ }
111
+ function lastCurrencyIndex(words, before) {
112
+ for (let i = before - 1; i >= 0; i--) {
113
+ if (words[i] === "real" || words[i] === "reais") return i + 1;
114
+ }
115
+ return before;
116
+ }
117
+
118
+ // src/views/payable-lines.ts
119
+ function parseLineItems(raw) {
120
+ if (raw === void 0 || raw === null) return { items: [] };
121
+ if (!Array.isArray(raw)) return { items: [], error: "`items` must be an array of table rows." };
122
+ const items = [];
123
+ for (const entry of raw) {
124
+ if (!entry || typeof entry !== "object") continue;
125
+ const row = entry;
126
+ const description = typeof row.description === "string" ? row.description.trim() : "";
127
+ const unitPrice = parseReceiptAmount(row.unitPrice);
128
+ if (!description || unitPrice === null) {
129
+ return {
130
+ items: [],
131
+ error: 'Every row needs a `description` and a readable `unitPrice` copied from the paper ("47,90"). Re-read the table and resend all rows.'
132
+ };
133
+ }
134
+ const quantityRaw = typeof row.quantity === "number" ? row.quantity : Number(row.quantity);
135
+ const quantity = Number.isFinite(quantityRaw) && quantityRaw > 0 ? quantityRaw : 1;
136
+ items.push({ description, quantity, unitPrice });
137
+ }
138
+ return { items };
139
+ }
140
+ function text(value) {
141
+ const s = typeof value === "string" ? value.trim() : "";
142
+ return s || void 0;
143
+ }
144
+ function today() {
145
+ const now = /* @__PURE__ */ new Date();
146
+ const pad = (n) => String(n).padStart(2, "0");
147
+ return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
148
+ }
149
+
150
+ // src/views/document-memory.ts
151
+ var lastDocument = null;
152
+ var DOCUMENT_TTL_MS = 15 * 60 * 1e3;
153
+ var STORAGE_KEY = "fayz:financial:last-document-read";
154
+ function readStored() {
155
+ try {
156
+ const raw = globalThis.sessionStorage?.getItem(STORAGE_KEY);
157
+ if (!raw) return null;
158
+ const parsed = JSON.parse(raw);
159
+ return parsed && typeof parsed.at === "number" && parsed.doc ? parsed : null;
160
+ } catch {
161
+ return null;
162
+ }
163
+ }
164
+ function writeStored(entry) {
165
+ try {
166
+ if (entry) globalThis.sessionStorage?.setItem(STORAGE_KEY, JSON.stringify(entry));
167
+ else globalThis.sessionStorage?.removeItem(STORAGE_KEY);
168
+ } catch {
169
+ }
170
+ }
171
+ function rememberedDocument() {
172
+ const entry = lastDocument ?? readStored();
173
+ if (!entry) return null;
174
+ if (Date.now() - entry.at > DOCUMENT_TTL_MS) {
175
+ clearRememberedDocument();
176
+ return null;
177
+ }
178
+ lastDocument = entry;
179
+ return entry.doc;
180
+ }
181
+ function clearRememberedDocument() {
182
+ lastDocument = null;
183
+ writeStored(null);
184
+ }
185
+ function normalizeDocument(args) {
186
+ const parsed = parseLineItems(args.items);
187
+ const items = parsed.error ? [] : parsed.items;
188
+ const shipping = parseReceiptAmount(args.shipping) ?? void 0;
189
+ const subtotal = parseReceiptAmount(args.subtotal) ?? void 0;
190
+ const total = parseReceiptAmount(args.total) ?? void 0;
191
+ const lineSum = items.length ? round2(items.reduce((s, i) => s + i.quantity * i.unitPrice, 0)) : void 0;
192
+ const computed = lineSum === void 0 ? void 0 : round2(lineSum + (shipping ?? 0));
193
+ const reconciles = computed === void 0 || total === void 0 ? false : Math.abs(computed - total) <= 0.02;
194
+ return {
195
+ items,
196
+ ...subtotal === void 0 ? {} : { subtotal },
197
+ ...shipping === void 0 ? {} : { shipping },
198
+ ...total === void 0 ? {} : { total },
199
+ ...lineSum === void 0 ? {} : { lineSum },
200
+ reconciles,
201
+ ...parseReceiptDate(args.documentDate) ? { documentDate: parseReceiptDate(args.documentDate) } : {},
202
+ // `null` é uma RESPOSTA: "o papel não diz". Diferente de "ainda não li".
203
+ dueDate: parseReceiptDate(args.dueDate) ?? null,
204
+ ...parseReceiptDate(args.paymentDate) ? { paymentDate: parseReceiptDate(args.paymentDate) } : {},
205
+ ...text(args.orderRef) ? { orderRef: text(args.orderRef) } : {},
206
+ ...parseReceiptDate(args.deliveryDate) ? { deliveryDate: parseReceiptDate(args.deliveryDate) } : {},
207
+ ...text(args.paymentMethod) ? { method: text(args.paymentMethod) } : {}
208
+ };
209
+ }
210
+ function rememberDocument(doc, meta) {
211
+ lastDocument = {
212
+ at: Date.now(),
213
+ doc,
214
+ ...meta?.source ? { source: meta.source } : {},
215
+ ...meta?.conversationId ? { conversationId: meta.conversationId } : {}
216
+ };
217
+ writeStored(lastDocument);
218
+ }
219
+ var RECEIPT_INTRO = {
220
+ role: "finance",
221
+ persona: "Leitor de recibos",
222
+ pitch: "Fotografe o recibo que eu leio e preencho a conta.",
223
+ description: "Leio o que est\xE1 no papel \u2014 fornecedor, valor e data \u2014, digo o que entendi e abro o cadastro j\xE1 preenchido para voc\xEA conferir. N\xE3o gravo nada sozinho: quem confirma \xE9 voc\xEA.",
224
+ suggestions: [
225
+ { label: "O que voc\xEA consegue ler de um recibo?" },
226
+ { label: "E se a foto estiver torta ou escura?" }
227
+ ]
228
+ };
229
+ var RECEIPT_INSTRUCTIONS = [
230
+ "A pessoa vai mandar a FOTO de um recibo ou nota. Leia a imagem e extraia apenas o que est\xE1 escrito nela.",
231
+ "Termine o turno CHAMANDO `openPayableDraft` com o que leu. Descrever o recibo em texto n\xE3o abre nada:",
232
+ "sem a chamada a pessoa fica olhando para uma frase e para nenhum formul\xE1rio. N\xE3o anuncie que vai abrir \u2014",
233
+ "abra.",
234
+ 'O valor vai COPIADO como est\xE1 escrito \u2014 "50,00", n\xE3o 5000. A v\xEDrgula \xE9 decimal em portugu\xEAs, e',
235
+ "convert\xEA-la voc\xEA mesmo j\xE1 transformou cinquenta reais em cinco mil.",
236
+ 'Se o papel escreve o valor POR EXTENSO ("CINQUENTA REAIS"), copie em `amountInWords`. Num recibo',
237
+ 'manuscrito \xE9 ele que manda: os algarismos v\xEAm com um tra\xE7o entre reais e centavos ("50|00") que n\xE3o',
238
+ "\xE9 v\xEDrgula, e \xE9 exatamente por isso que o recibo escreve o valor duas vezes.",
239
+ "Preencha os campos NESTA ORDEM antes de chamar: valor, data, descri\xE7\xE3o, fornecedor \u2014 e s\xF3 ent\xE3o",
240
+ "`notes`, com o que sobrou. `notes` \xE9 o resto, nunca o lugar de algo que tem campo pr\xF3prio: a data",
241
+ 'escrita como "Rio, 20/01/22" \xE9 a DATA, e "proveniente da presta\xE7\xE3o de servi\xE7o de t\xE1xi" \xE9 a',
242
+ "DESCRI\xC7\xC3O. Enfiadas nas observa\xE7\xF5es, elas somem dos campos onde a pessoa vai procur\xE1-las.",
243
+ 'N\xC3O confunda as linhas do recibo: "a import\xE2ncia de CINQUENTA REAIS" \xE9 o VALOR por extenso, n\xE3o',
244
+ "um fornecedor chamado Cinquenta Reais. Sem um nome claro de quem recebeu, deixe `supplier` de fora.",
245
+ "Ponha em `notes` tudo o mais que est\xE1 escrito e n\xE3o coube nos outros campos: itiner\xE1rio, placa,",
246
+ "n\xFAmero do documento, quem assinou. \xC9 onde a pessoa vai procurar o que o papel dizia.",
247
+ "N\xC3O invente campo que o papel n\xE3o responde. Vencimento, categoria e conta de pagamento quase nunca est\xE3o",
248
+ "num recibo: deixe de fora. Campo vazio custa um toque; campo errado custa uma confer\xEAncia inteira.",
249
+ "S\xF3 existe uma raz\xE3o para N\xC3O chamar: a foto estar ileg\xEDvel ou o valor total amb\xEDguo. A\xED pergunte \u2014 e",
250
+ "pergunte s\xF3 isso, sem descrever o resto."
251
+ ].join(" ");
252
+ var OPEN_DRAFT = {
253
+ id: "financial.receipt.open-draft",
254
+ name: "openPayableDraft",
255
+ description: 'Opens the "new payable" form pre-filled with what was read from a receipt photo. Call this as soon as you can read an amount \u2014 it is how a receipt turns into a record, and describing the receipt in prose instead leaves the person with a sentence and no form. Writes nothing: the person reviews the form and saves it. Send ONLY fields the receipt actually states \u2014 omit anything you would be guessing at.',
256
+ icon: "ReceiptText",
257
+ mode: "read",
258
+ execution: { plane: "client" },
259
+ category: "Finance",
260
+ role: "finance",
261
+ parameters: {
262
+ type: "object",
263
+ properties: {
264
+ supplier: {
265
+ type: "string",
266
+ description: 'Quem RECEBEU o dinheiro, se o papel nomear. Cuidado: "Recebi de" e "a import\xE2ncia de" n\xE3o s\xE3o nomes \u2014 a primeira \xE9 quem pagou e a segunda \xE9 o valor por extenso. Um recibo de t\xE1xi normalmente n\xE3o tem fornecedor nenhum: omita em vez de inventar.'
267
+ },
268
+ amount: {
269
+ type: "string",
270
+ description: 'O valor total COPIADO LETRA POR LETRA do papel, com a pontua\xE7\xE3o que estiver l\xE1: "50,00", "1.234,56", "R$ 89,90". N\xC3O converta para n\xFAmero e n\xE3o tire a v\xEDrgula \u2014 quem l\xEA a pontua\xE7\xE3o \xE9 o aplicativo, que sabe a moeda do pa\xEDs.'
271
+ },
272
+ amountInWords: {
273
+ type: "string",
274
+ description: 'O valor POR EXTENSO, se o papel escrever ("CINQUENTA REAIS", "cento e vinte reais e cinquenta centavos"). Copie as palavras como est\xE3o. Num recibo manuscrito \xE9 ele que desempata: os algarismos costumam vir com um tra\xE7o entre reais e centavos ("50|00"), que n\xE3o \xE9 v\xEDrgula nenhuma.'
275
+ },
276
+ date: {
277
+ type: "string",
278
+ description: 'A data do documento como est\xE1 escrita ("20/01/2022", "2022-01-20"). Omita se o papel n\xE3o diz.'
279
+ },
280
+ description: {
281
+ type: "string",
282
+ description: 'O que foi pago, em poucas palavras \u2014 \xE9 o nome do item na conta. Um recibo quase sempre diz ("presta\xE7\xE3o de servi\xE7o de t\xE1xi", "insumos de padaria"). Sem ele a conta nasce como "Sem descri\xE7\xE3o", que n\xE3o diz nada a quem for conferir depois.'
283
+ },
284
+ notes: {
285
+ type: "string",
286
+ description: "Todo o RESTO que est\xE1 escrito e n\xE3o coube nos campos acima: itiner\xE1rio, placa, n\xFAmero do documento, condi\xE7\xF5es, nome de quem assinou. Vai para as observa\xE7\xF5es da conta, que \xE9 onde a pessoa procura o que o papel dizia. Copie o que leu; n\xE3o resuma em uma palavra. N\xC3O repita aqui o que j\xE1 foi para `date`, `amount` ou `description` \u2014 o resto significa o resto."
287
+ }
288
+ },
289
+ required: ["amount"]
290
+ }
291
+ };
292
+ var RECEIVABLE_RECEIPT_INTRO = {
293
+ role: "finance",
294
+ persona: "Leitor de notas e comprovantes",
295
+ pitch: "Envie a nota que voc\xEA emitiu e eu preencho a conta a receber.",
296
+ description: "Leio o que est\xE1 no documento \u2014 cliente, valor, data, n\xFAmero e chave da NF-e \u2014, digo o que entendi e abro a conta a receber j\xE1 preenchida para voc\xEA conferir. N\xE3o gravo nada sozinho: quem confirma \xE9 voc\xEA.",
297
+ suggestions: [
298
+ { label: "O que voc\xEA consegue ler de uma nota fiscal?" },
299
+ { label: "E se a foto estiver torta ou escura?" }
300
+ ]
301
+ };
302
+ var RECEIVABLE_RECEIPT_INSTRUCTIONS = [
303
+ "The person will send a PHOTO or PDF of a document for money they are going to RECEIVE: usually an NF-e/NFS-e",
304
+ "they ISSUED to a customer, or a receipt they gave. Read it and extract only what is written on it.",
305
+ "End the turn by CALLING `openReceivableDraft` with what you read. Describing the document in text opens nothing.",
306
+ "Do not announce that you will open it \u2014 open it. Talk to the person in Brazilian Portuguese.",
307
+ "THE COUNTERPARTY IS THE CUSTOMER. On an NF-e the person issued, the EMITENTE (issuer) is the person's own",
308
+ "company: never send it as the customer. Send the DESTINAT\xC1RIO / TOMADOR (recipient) name in `customer`.",
309
+ 'On a receipt, the customer is the one who PAID ("Recebi de ..."). Without a clear customer name, omit `customer`.',
310
+ 'Copy `amount` exactly as printed ("1.234,56"), never converted \u2014 the app parses the punctuation.',
311
+ "If the paper spells the amount out in words, copy it into `amountInWords`.",
312
+ "If it is an NF-e, copy the note NUMBER into `fiscalNumber` and the 44-digit ACCESS KEY (chave de acesso) into",
313
+ "`accessKey`, digits only. Omit either one when the paper does not show it.",
314
+ "Use `date` for the issue date, `description` for what was sold, and `notes` for anything else written that has",
315
+ "no field of its own. Do not invent fields the paper does not answer.",
316
+ "The only reason NOT to call is an illegible photo or an ambiguous total: then ask only that."
317
+ ].join(" ");
318
+ var OPEN_RECEIVABLE_DRAFT = {
319
+ id: "financial.receipt.open-receivable-draft",
320
+ name: "openReceivableDraft",
321
+ description: 'Opens the "new receivable" form pre-filled with what was read from an issued invoice (NF-e) or receipt. Call this as soon as you can read an amount. Writes nothing: the person reviews the form and saves it. Send ONLY fields the document actually states \u2014 omit anything you would be guessing at.',
322
+ icon: "ReceiptText",
323
+ mode: "read",
324
+ execution: { plane: "client" },
325
+ category: "Finance",
326
+ role: "finance",
327
+ parameters: {
328
+ type: "object",
329
+ properties: {
330
+ customer: {
331
+ type: "string",
332
+ description: `Who OWES / PAID the money: the DESTINAT\xC1RIO or TOMADOR on an NF-e, or "Recebi de ..." on a receipt. NEVER the EMITENTE of an NF-e \u2014 that is the person's own company. Omit when not named.`
333
+ },
334
+ amount: {
335
+ type: "string",
336
+ description: 'The total COPIED as printed, punctuation included ("50,00", "1.234,56"). Do not convert it.'
337
+ },
338
+ amountInWords: {
339
+ type: "string",
340
+ description: "The amount spelled out in words, if the paper writes it. Copy the words as they are."
341
+ },
342
+ date: {
343
+ type: "string",
344
+ description: 'The issue date as written ("20/01/2026", "2026-01-20"). Omit if absent.'
345
+ },
346
+ description: {
347
+ type: "string",
348
+ description: "What was sold or provided, in a few words. It becomes the item name on the receivable."
349
+ },
350
+ fiscalNumber: {
351
+ type: "string",
352
+ description: 'The NF-e / NFS-e number (e.g. "1234" or "000.001.234"), when the document is a fiscal note.'
353
+ },
354
+ accessKey: {
355
+ type: "string",
356
+ description: 'The NF-e 44-digit access key ("chave de acesso"), when printed. Digits only.'
357
+ },
358
+ notes: {
359
+ type: "string",
360
+ description: "Everything ELSE written that has no field above. Do not repeat the amount, date, description, number or key."
361
+ }
362
+ },
363
+ required: ["amount"]
364
+ }
365
+ };
366
+ function parseAccessKey(raw) {
367
+ if (typeof raw !== "string") return void 0;
368
+ const digits = raw.replace(/\D/g, "");
369
+ return digits.length === 44 ? digits : void 0;
370
+ }
371
+ function parseReceiptAmount(raw) {
372
+ if (typeof raw === "number") return Number.isFinite(raw) && raw > 0 ? raw : null;
373
+ if (typeof raw !== "string") return null;
374
+ const text2 = raw.replace(/[^\d.,-]/g, "").trim();
375
+ if (!text2 || !/\d/.test(text2)) return null;
376
+ const lastComma = text2.lastIndexOf(",");
377
+ const lastDot = text2.lastIndexOf(".");
378
+ const cut = Math.max(lastComma, lastDot);
379
+ let normalized;
380
+ if (cut < 0) {
381
+ normalized = text2;
382
+ } else {
383
+ const decimals = text2.length - cut - 1;
384
+ normalized = decimals === 3 ? text2.replace(/[.,]/g, "") : `${text2.slice(0, cut).replace(/[.,]/g, "")}.${text2.slice(cut + 1)}`;
385
+ }
386
+ const value = Number(normalized);
387
+ return Number.isFinite(value) && value > 0 ? value : null;
388
+ }
389
+ function parseReceiptDate(raw) {
390
+ const text2 = typeof raw === "string" ? raw.trim() : "";
391
+ if (!text2) return void 0;
392
+ if (/^\d{4}-\d{2}-\d{2}$/.test(text2)) return isRealDate(text2) ? text2 : void 0;
393
+ const br = text2.match(/^(\d{1,2})[/.-](\d{1,2})[/.-](\d{2}|\d{4})$/);
394
+ if (!br) return void 0;
395
+ const [, d, m, y] = br;
396
+ const year = y.length === 2 ? `20${y}` : y;
397
+ const iso = `${year}-${m.padStart(2, "0")}-${d.padStart(2, "0")}`;
398
+ return isRealDate(iso) ? iso : void 0;
399
+ }
400
+ function findDateIn(text2) {
401
+ if (typeof text2 !== "string") return void 0;
402
+ return text2.match(/\b\d{1,2}[/.-]\d{1,2}[/.-](?:\d{4}|\d{2})\b/)?.[0];
403
+ }
404
+ function isRealDate(iso) {
405
+ const [y, m, d] = iso.split("-").map(Number);
406
+ const date = new Date(Date.UTC(y, m - 1, d));
407
+ return date.getUTCFullYear() === y && date.getUTCMonth() === m - 1 && date.getUTCDate() === d;
408
+ }
409
+ var pending = null;
410
+ function consumePayableDraft(direction) {
411
+ const draft = pending;
412
+ pending = null;
413
+ if (draft && direction && (draft.direction ?? "debit") !== direction) return null;
414
+ return draft;
415
+ }
416
+ var lastPhoto;
417
+ function rememberReceiptPhoto(dataUrl) {
418
+ lastPhoto = dataUrl;
419
+ }
420
+ function lastReceiptPhoto() {
421
+ return lastPhoto;
422
+ }
423
+ function readAmount(args) {
424
+ if (typeof args.amount !== "string") {
425
+ return {
426
+ ok: false,
427
+ error: "amount_must_be_literal",
428
+ message: 'Send `amount` as the TEXT printed on the receipt, punctuation included ("50,00", "1.234,56") \u2014 never as a number. A decimal comma is a thousands separator in English, so converting it yourself turns fifty into five thousand. Read the paper again and resend.'
429
+ };
430
+ }
431
+ const figures = parseReceiptAmount(args.amount);
432
+ const spelled = parseAmountInWords(args.amountInWords);
433
+ const amount = spelled ?? figures;
434
+ if (amount === null) {
435
+ return {
436
+ ok: false,
437
+ error: "invalid_amount",
438
+ message: "That is not a readable amount. If the total is illegible on the photo, ask the person."
439
+ };
440
+ }
441
+ return { ok: true, amount, spelled, figures };
442
+ }
443
+ function trimmed(value) {
444
+ const s = typeof value === "string" ? value.trim() : "";
445
+ return s || void 0;
446
+ }
447
+ function createReceivableReceiptTools({ onDraft, getReceiptUrl }) {
448
+ return [
449
+ {
450
+ tool: OPEN_RECEIVABLE_DRAFT,
451
+ // The payable path's `rememberedDocument` is NOT read here: it is a
452
+ // supplier-side reading, and seeding a receivable from it would bring the
453
+ // issuer back as the counterparty.
454
+ execute: async (args) => {
455
+ const read = readAmount(args);
456
+ if (!read.ok) return read;
457
+ const accessKey = parseAccessKey(args.accessKey);
458
+ const notes = [
459
+ trimmed(args.notes),
460
+ accessKey ? `Chave de acesso NF-e: ${accessKey}` : void 0
461
+ ].filter(Boolean).join(" \xB7 ") || void 0;
462
+ pending = {
463
+ direction: "credit",
464
+ amount: read.amount,
465
+ customer: trimmed(args.customer),
466
+ description: trimmed(args.description),
467
+ fiscalNumber: trimmed(args.fiscalNumber),
468
+ accessKey,
469
+ notes,
470
+ date: parseReceiptDate(args.date) ?? parseReceiptDate(findDateIn(args.notes)),
471
+ receiptUrl: getReceiptUrl()
472
+ };
473
+ onDraft();
474
+ revealPage();
475
+ return {
476
+ ok: true,
477
+ opened: true,
478
+ ...read.spelled !== null && read.figures !== null && Math.abs(read.spelled - read.figures) > 5e-3 ? {
479
+ amountFromWords: read.spelled,
480
+ amountFromFigures: read.figures,
481
+ note: `The figures looked like ${read.figures} and the words say ${read.spelled}. The words were used \u2014 tell the person.`
482
+ } : {},
483
+ filled: Object.entries(pending).filter(([, v]) => v !== void 0 && v !== "").map(([k]) => k)
484
+ };
485
+ }
486
+ }
487
+ ];
488
+ }
489
+ function createReceiptTools({ onDraft, getReceiptUrl = lastReceiptPhoto, direction = "payable" }) {
490
+ if (direction === "receivable") return createReceivableReceiptTools({ onDraft, getReceiptUrl });
491
+ return [
492
+ {
493
+ tool: OPEN_DRAFT,
494
+ execute: async (args) => {
495
+ const read = rememberedDocument();
496
+ if (read && read.total !== void 0) {
497
+ pending = {
498
+ amount: read.total,
499
+ ...read.items.length ? { items: read.items } : {},
500
+ ...read.shipping === void 0 ? {} : { shipping: read.shipping },
501
+ supplier: text(args.supplier) ?? void 0,
502
+ description: text(args.description) ?? void 0,
503
+ notes: [
504
+ text(args.notes),
505
+ read.orderRef ? `Pedido: ${read.orderRef}` : null,
506
+ read.deliveryDate ? `Entrega: ${read.deliveryDate}` : null
507
+ ].filter(Boolean).join(" \xB7 ") || void 0,
508
+ date: read.documentDate ?? parseReceiptDate(args.date),
509
+ receiptUrl: getReceiptUrl()
510
+ };
511
+ onDraft();
512
+ revealPage();
513
+ return {
514
+ ok: true,
515
+ opened: true,
516
+ fromDocument: true,
517
+ items: read.items.length,
518
+ total: read.total,
519
+ message: `O cadastro abriu com o que foi LIDO do documento \u2014 ${read.items.length || 1} linha(s), total ${read.total}. Diga isso em UMA frase e pare. N\xE3o repita os valores campo a campo e N\xC3O os recalcule: a pessoa est\xE1 olhando para eles.`
520
+ };
521
+ }
522
+ if (typeof args.amount !== "string") {
523
+ return {
524
+ ok: false,
525
+ error: "amount_must_be_literal",
526
+ message: 'Send `amount` as the TEXT printed on the receipt, punctuation included ("50,00", "1.234,56") \u2014 never as a number. A decimal comma is a thousands separator in English, so converting it yourself turns fifty into five thousand. Read the paper again and resend.'
527
+ };
528
+ }
529
+ const figures = parseReceiptAmount(args.amount);
530
+ const spelled = parseAmountInWords(args.amountInWords);
531
+ const amount = spelled ?? figures;
532
+ if (amount === null) {
533
+ return {
534
+ ok: false,
535
+ error: "invalid_amount",
536
+ message: "That is not a readable amount. If the total is illegible on the photo, ask the person."
537
+ };
538
+ }
539
+ const text2 = (value) => {
540
+ const s = typeof value === "string" ? value.trim() : "";
541
+ return s || void 0;
542
+ };
543
+ const isoDate = parseReceiptDate(args.date) ?? parseReceiptDate(findDateIn(args.notes));
544
+ pending = {
545
+ amount,
546
+ supplier: text2(args.supplier),
547
+ description: text2(args.description),
548
+ notes: text2(args.notes),
549
+ date: isoDate,
550
+ receiptUrl: getReceiptUrl()
551
+ };
552
+ onDraft();
553
+ revealPage();
554
+ return {
555
+ ok: true,
556
+ opened: true,
557
+ // Divergiram: o formulário abriu com o extenso, e quem assina merece
558
+ // saber que os algarismos diziam outra coisa.
559
+ ...spelled !== null && figures !== null && Math.abs(spelled - figures) > 5e-3 ? {
560
+ amountFromWords: spelled,
561
+ amountFromFigures: figures,
562
+ note: `Os algarismos pareciam ${figures} e o extenso diz ${spelled}. Usei o extenso, que \xE9 o que vale num recibo \u2014 diga isso \xE0 pessoa.`
563
+ } : {},
564
+ filled: Object.entries(pending).filter(([, v]) => v !== void 0 && v !== "").map(([k]) => k)
565
+ };
566
+ }
567
+ }
568
+ ];
569
+ }
570
+ function startReceiptCapture(labels) {
571
+ rememberReceiptPhoto(void 0);
572
+ askAI({ hint: labels.ask, intro: labels.direction === "receivable" ? RECEIVABLE_RECEIPT_INTRO : RECEIPT_INTRO });
573
+ requestChatUpload({
574
+ label: labels.uploadLabel,
575
+ description: labels.uploadHint,
576
+ accept: "image/*,application/pdf",
577
+ capture: "environment",
578
+ onFiles: async (files) => {
579
+ const photo = files[0];
580
+ if (!photo) return;
581
+ if (photo.type.startsWith("image/")) {
582
+ const inline = await toInlineFile(photo);
583
+ rememberReceiptPhoto(`data:${inline.mime};base64,${inline.data}`);
584
+ }
585
+ useChatStore.getState().queuePrompt(labels.prompt, [photo]);
586
+ }
587
+ });
588
+ }
589
+ var PAYABLE_INSTRUCTIONS = [
590
+ 'Uma "conta a pagar" \xE9 uma DESPESA com um FORNECEDOR \u2014 quem a empresa deve. Para registrar uma,',
591
+ "chame `createPayable`. NUNCA use `createRecord` em `financial:payment-methods` para isso: aquele",
592
+ "registro configura como os CLIENTES pagam a empresa (bandeira, taxa, parcelamento), e um",
593
+ "fornecedor cadastrado l\xE1 n\xE3o vira conta nenhuma \u2014 vira lixo na configura\xE7\xE3o da loja.",
594
+ "Se a pessoa mandar a FOTO de um recibo ou de uma nota, leia a imagem e use o que est\xE1 escrito",
595
+ "nela. Uma foto n\xE3o tem colunas: nunca proponha importa\xE7\xE3o para ela.",
596
+ "Num pedido ou nota com TABELA, leia a tabela LINHA A LINHA e mande todas em `items` \u2014 c\xF3digo,",
597
+ 'descri\xE7\xE3o, quantidade e pre\xE7o unit\xE1rio de cada uma. Uma linha gen\xE9rica de "servi\xE7o prestado" com',
598
+ "o total dentro perde o pedido inteiro. O FRETE \xE9 campo pr\xF3prio (`shipping`), n\xE3o some ao produto.",
599
+ "`amount` \xE9 sempre o TOTAL do rodap\xE9 (com frete), nunca o subtotal e nunca o total de uma linha.",
600
+ 'Capture tamb\xE9m "M\xE9todo de pagamento:" quando impresso, o N\xDAMERO DO PEDIDO ("Pedido: #164223") e a',
601
+ "data de ENTREGA \u2014 pedido e entrega v\xE3o em `notes`, que \xE9 onde a pessoa procura o que o papel dizia.",
602
+ 'Se o texto trouxer uma linha como "[c\xF3digo Pix copia-e-cola \u2014 valor embutido: X]", aquilo \xE9 o',
603
+ "c\xF3digo do pagamento: X confirma o total, e os d\xEDgitos em volta N\xC3O s\xE3o pre\xE7os.",
604
+ '"Expira em" de um c\xF3digo Pix \xE9 a validade do C\xD3DIGO, n\xE3o o vencimento da conta. Um pedido pago \xE0',
605
+ "vista n\xE3o tem vencimento: deixe `dueDate` de fora e o formul\xE1rio pergunta. Data chutada custa uma",
606
+ "cobran\xE7a; campo vazio custa um toque.",
607
+ "LEU UM PAPEL? PASSO 0 \xE9 `resolvePayableContext`, ANTES de escrever qualquer coisa. Extraia TUDO o",
608
+ "que est\xE1 escrito \u2014 fornecedor, CNPJ, o que foi comprado, forma de pagamento, bandeira e final do",
609
+ "cart\xE3o, datas, valores \u2014 e mande para conferir contra o que este neg\xF3cio j\xE1 tem. Ele responde o",
610
+ "que existe e o que falta, sem gravar nada. Diga o resultado em uma linha curta por depend\xEAncia e",
611
+ "resolva as que faltam NA ORDEM que ele devolve: fornecedor, forma de pagamento, depois a conta,",
612
+ "depois a baixa. Se voltar um candidato parecido, PERGUNTE se \xE9 o mesmo \u2014 n\xE3o cadastre de novo.",
613
+ 'Valor vai COPIADO como est\xE1 escrito ("50,00", "1.234,56"), nunca convertido \u2014 a v\xEDrgula \xE9',
614
+ "decimal em portugu\xEAs, e convert\xEA-la voc\xEA mesmo j\xE1 transformou cinquenta reais em cinco mil.",
615
+ "N\xE3o entreviste a pessoa campo a campo. Mande o que ela disse e chame a ferramenta: o que abre",
616
+ "\xE9 um FORMUL\xC1RIO edit\xE1vel, e o que faltar ela preenche ali \u2014 em um toque, olhando os campos.",
617
+ "Uma conta a pagar tem TR\xCAS passos, cada um confirmado pela pessoa: (1) o fornecedor, (2) o t\xEDtulo,",
618
+ "(3) o pagamento \u2014 e o terceiro s\xF3 existe se a conta J\xC1 foi paga. Se o recibo ou a pessoa disser",
619
+ 'que j\xE1 pagou ("pago hoje", "paguei no Pix"), mande `paid: true`; a ferramenta cuida da ordem.',
620
+ "Quando faltar saber COMO foi pago, a ferramenta PERGUNTA com as op\xE7\xF5es do pr\xF3prio neg\xF3cio na tela.",
621
+ "N\xE3o repita as op\xE7\xF5es em texto e n\xE3o escolha por ela: espere a resposta e ent\xE3o chame",
622
+ "`registerPayablePayment` com o id que ela tocou.",
623
+ "REGRA DE PRECED\xCANCIA: se o cadastro de conta a pagar estiver ABERTO na tela com um rascunho (veja",
624
+ '"Tela aberta agora"), ele \xE9 a conta em curso. Complete-o com `fillPayableDraft` e salve com',
625
+ "`submitPayableDraft`. `createPayable` \xE9 s\xF3 para quando N\xC3O h\xE1 rascunho aberto \u2014 cham\xE1-la com o",
626
+ "formul\xE1rio na tela cria uma segunda conta e deixa a pessoa num rascunho \xF3rf\xE3o.",
627
+ "Depois de lan\xE7ar, a tela da conta abre sozinha \u2014 \xE9 o normal e n\xE3o precisa ser anunciado. Mande",
628
+ "`redirect: false` s\xF3 quando levar a pessoa embora atrapalharia: v\xE1rias contas em sequ\xEAncia, ou",
629
+ "algu\xE9m no meio de outra tarefa que s\xF3 passou a conta de lado.",
630
+ '"Marca como pago", "paguei", "foi pago" sobre uma conta que VOC\xCA acabou de lan\xE7ar \xE9 sempre',
631
+ "`registerPayablePayment` com o invoiceId daquela conta \u2014 NUNCA `createPayable` de novo. Lan\xE7ar a",
632
+ "mesma despesa duas vezes \xE9 dinheiro contado em dobro, e desfazer isso \xE9 trabalho de concilia\xE7\xE3o."
633
+ ].join(" ");
634
+ var CREATE_PAYABLE = {
635
+ id: "financial.create-payable",
636
+ name: "createPayable",
637
+ description: 'Records a BILL THE BUSINESS OWES a supplier (conta a pagar, despesa, boleto, invoice from a vendor). This is the tool for "lan\xE7ar/salvar uma conta a pagar", "paguei o fornecedor X", "recibo de despesa". It walks a CHECKLIST, one confirmable step at a time: (1) the supplier \u2014 found by name or created as a supplier contact, (2) the bill itself. Each step opens an EDITABLE FORM pre-filled with what you send and writes nothing until the person confirms it \u2014 so call it as soon as you have an amount and let the forms ask for the rest. If the receipt says it was ALREADY PAID ("pago hoje", "paguei via Pix"), set `paid: true`: step (3) is the payment, and this tool will either ask HOW it was paid or hand you off to `registerPayablePayment`. Send ONLY what you were actually told or could read; omit anything you would be guessing at. When the bill came off a receipt/photo/PDF, call `resolvePayableContext` FIRST and settle the missing dependencies it reports before calling this. A document with a TABLE goes in row by row through `items` (plus `shipping` for frete): the lines are reconciled against `amount`, and a mismatch comes back to be asked about rather than silently trusted. DO NOT use this when a payable form is already open on screen (a draft read from a receipt or PDF): that draft is the bill in progress \u2014 use `fillPayableDraft` and `submitPayableDraft` instead, or this writes a second, duplicate bill and strands the person on a draft that no longer means anything.',
638
+ icon: "ReceiptText",
639
+ mode: "persist",
640
+ execution: { plane: "client" },
641
+ // O guard é desta ferramenta: ela abre o próprio cartão de confirmação, que é
642
+ // um formulário. O cartão genérico do loop seria um segundo "tem certeza?"
643
+ // antes do formulário — duas perguntas para uma decisão.
644
+ requiresConfirmation: false,
645
+ category: "Finance",
646
+ role: "finance",
647
+ permission: { feature: "financial", action: "create" },
648
+ limitKey: "movements_month",
649
+ parameters: {
650
+ type: "object",
651
+ properties: {
652
+ supplier: {
653
+ type: "string",
654
+ description: 'Quem RECEBE o dinheiro \u2014 o fornecedor, a empresa, o prestador. \xC9 um NOME de quem foi pago ("Printi", "Padaria do Z\xE9"). N\xE3o \xE9 o que foi comprado nem o valor por extenso.'
655
+ },
656
+ amount: {
657
+ type: "string",
658
+ description: 'O valor TOTAL da conta \u2014 o que vai ser pago, incluindo frete. Num pedido \xE9 o "Total" do rodap\xE9, nunca o subtotal e nunca o total de uma linha. COPIADO LETRA POR LETRA, com a pontua\xE7\xE3o que estiver escrita: "50,00", "1.234,56", "R$ 89,90". N\xC3O converta para n\xFAmero e n\xE3o tire a v\xEDrgula \u2014 quem l\xEA a pontua\xE7\xE3o \xE9 o aplicativo, que sabe a moeda do pa\xEDs.'
659
+ },
660
+ amountInWords: {
661
+ type: "string",
662
+ description: 'O valor POR EXTENSO, quando o papel escrever ("CINQUENTA REAIS"). Num recibo manuscrito \xE9 ele que desempata os algarismos. Copie as palavras como est\xE3o.'
663
+ },
664
+ dueDate: {
665
+ type: "string",
666
+ description: 'O VENCIMENTO, como est\xE1 escrito ("20/01/2026", "2026-01-20"). Omita se ningu\xE9m disse \u2014 o formul\xE1rio pergunta. Uma data de vencimento chutada custa uma confer\xEAncia inteira.'
667
+ },
668
+ date: {
669
+ type: "string",
670
+ description: 'A data do DOCUMENTO (quando a compra aconteceu), se souber. "hoje" \xE9 hoje.'
671
+ },
672
+ description: {
673
+ type: "string",
674
+ description: 'O que foi comprado ou pago, em poucas palavras \u2014 \xE9 o nome da linha da conta ("cart\xF5es de visita", "insumos de padaria"). Sem ela a conta nasce sem dizer o que \xE9.'
675
+ },
676
+ items: {
677
+ type: "array",
678
+ description: 'As LINHAS da nota, uma por linha da tabela \u2014 leia a tabela linha a linha e copie todas. Um pedido de tr\xEAs produtos vira tr\xEAs linhas aqui, n\xE3o uma linha gen\xE9rica de "servi\xE7o prestado": \xE9 assim que a conta diz depois o que foi comprado. Omita quando o papel n\xE3o tem tabela (um recibo de t\xE1xi \xE9 uma linha s\xF3, e a\xED basta `description`).',
679
+ items: {
680
+ type: "object",
681
+ properties: {
682
+ description: { type: "string", description: "O que \xE9 a linha, como est\xE1 escrito (c\xF3digo + nome)." },
683
+ quantity: { type: "number", description: "A quantidade da linha. 1 quando a tabela n\xE3o diz." },
684
+ unitPrice: {
685
+ type: "string",
686
+ description: 'O pre\xE7o UNIT\xC1RIO copiado letra por letra ("47,90"), n\xE3o o total da linha e n\xE3o convertido.'
687
+ }
688
+ },
689
+ required: ["description", "unitPrice"]
690
+ }
691
+ },
692
+ shipping: {
693
+ type: "string",
694
+ description: 'O FRETE, copiado como est\xE1 escrito ("52,19"), quando o papel o cobra em separado. Vira uma linha pr\xF3pria na conta \u2014 somado a um produto ele desapareceria dentro do pre\xE7o dele.'
695
+ },
696
+ notes: {
697
+ type: "string",
698
+ description: 'Todo o RESTO que foi dito e n\xE3o coube nos campos acima: N\xDAMERO DO PEDIDO ou do documento ("Pedido: #164223"), data de entrega, condi\xE7\xF5es, para qual projeto. \xC9 onde a pessoa vai procurar o que o papel dizia. N\xC3O repita aqui o que j\xE1 foi para os outros campos.'
699
+ },
700
+ paid: {
701
+ type: "boolean",
702
+ description: 'true quando a conta J\xC1 FOI PAGA \u2014 "pago hoje", "paguei via Pix", um recibo quitado. Uma conta paga tem um terceiro passo: registrar o pagamento. N\xE3o marque true por um boleto ou uma nota que ainda vai vencer.'
703
+ },
704
+ paymentMethod: {
705
+ type: "string",
706
+ description: 'COMO foi pago, se a pessoa disse ("Pix", "dinheiro", "cart\xE3o de cr\xE9dito"). N\xE3o chute: sem isto a ferramenta PERGUNTA, oferecendo as formas que este neg\xF3cio aceita.'
707
+ },
708
+ paymentDate: {
709
+ type: "string",
710
+ description: 'QUANDO foi pago, se disserem. "pago hoje" \xE9 hoje.'
711
+ },
712
+ redirect: {
713
+ type: "boolean",
714
+ description: "Levar a pessoa at\xE9 a conta rec\xE9m-criada (default: true). Deixe true no caso normal \u2014 quem acabou de lan\xE7ar uma conta quer v\xEA-la. Mande FALSE quando levar embora atrapalharia: ela est\xE1 lan\xE7ando V\xC1RIAS contas em sequ\xEAncia, est\xE1 no meio de outra tarefa (uma tela de cadastro, uma concilia\xE7\xE3o) e s\xF3 passou a conta de lado, ou pediu explicitamente para ficar onde est\xE1. Com false a conta \xE9 criada do mesmo jeito e o link volta na resposta."
715
+ }
716
+ },
717
+ // `amount` deixa de ser obrigatório SÓ porque o passo 0 pode tê-lo lido: sem
718
+ // leitura guardada a ferramenta recusa do mesmo jeito, com a mesma mensagem.
719
+ required: []
720
+ }
721
+ };
722
+ var REGISTER_PAYMENT = {
723
+ id: "financial.register-payable-payment",
724
+ name: "registerPayablePayment",
725
+ description: "Step 3 of the payable checklist: registers that a bill WAS PAID \u2014 the money leaving, with its method and date. Call it with the `invoiceId` a previous `createPayable` returned, and the `method` the person chose (send the option id when they tapped one). Opens an editable confirmation before writing. Use it whenever a bill already recorded turns out to be settled; never to create the bill itself.",
726
+ icon: "BadgeDollarSign",
727
+ mode: "persist",
728
+ execution: { plane: "client" },
729
+ requiresConfirmation: false,
730
+ category: "Finance",
731
+ role: "finance",
732
+ permission: { feature: "financial", action: "update" },
733
+ parameters: {
734
+ type: "object",
735
+ properties: {
736
+ invoiceId: { type: "string", description: "Id da conta a pagar, devolvido por createPayable." },
737
+ method: {
738
+ type: "string",
739
+ description: 'A forma de pagamento \u2014 o id da op\xE7\xE3o que a pessoa tocou, ou o nome dela ("Pix", "Dinheiro").'
740
+ },
741
+ paymentDate: { type: "string", description: "A data do pagamento. Omita para hoje." }
742
+ },
743
+ required: ["invoiceId"]
744
+ }
745
+ };
746
+ var OPEN_FULL_FORM = {
747
+ id: "financial.open-payable-form",
748
+ name: "openPayableForm",
749
+ description: 'Opens the full "new payable" screen, empty or pre-filled, for a bill that needs more than a one-line entry: several installments, a receipt to attach, a ledger account or cost centre. Writes nothing. Prefer `createPayable` for an ordinary single bill \u2014 this one is the long form.',
750
+ icon: "FilePlus",
751
+ mode: "read",
752
+ execution: { plane: "client" },
753
+ category: "Finance",
754
+ role: "finance",
755
+ permission: { feature: "financial", action: "create" },
756
+ parameters: { type: "object", properties: {} }
757
+ };
758
+ function supplierRef(id, label) {
759
+ if (!resolveEntityRoute("person", "supplier")) return {};
760
+ return { ref: { id, label, archetype: "person:supplier" } };
761
+ }
762
+ async function resolveSupplier(lookup, name) {
763
+ const wanted = name.trim().toLowerCase();
764
+ const hits = await lookup.search(name).catch(() => []);
765
+ const exact = hits.find((r) => r.label.trim().toLowerCase() === wanted);
766
+ if (exact) return { ok: true, id: exact.id, name: exact.label, created: false };
767
+ if (!lookup.create) return { ok: true, name, created: false };
768
+ const decision = await useChatStore.getState().requestConfirmation({
769
+ id: `supplier-${Date.now()}`,
770
+ toolName: "createPayable",
771
+ title: "Novo fornecedor",
772
+ params: { name },
773
+ plane: "client",
774
+ scope: "financial:supplier",
775
+ scopeLabel: "Cadastrar fornecedor",
776
+ fields: [
777
+ { key: "name", label: "Nome", type: "text", required: true },
778
+ { key: "document_number", label: "CNPJ / CPF", type: "text" },
779
+ { key: "phone", label: "Telefone", type: "text" },
780
+ { key: "email", label: "E-mail", type: "text" }
781
+ ]
782
+ });
783
+ if (!decision.approved) return { ok: false, cancelled: true };
784
+ const draft = { ...{ name }, ...decision.values ?? {} };
785
+ const finalName = text(draft.name) ?? name;
786
+ const made = await lookup.create({
787
+ ...draft,
788
+ name: finalName
789
+ });
790
+ return { ok: true, id: made.id, name: made.label, created: true };
791
+ }
792
+ async function paymentOptions(provider) {
793
+ const types = await provider.getPaymentMethodTypes().catch(() => []);
794
+ return types.filter((t) => t.isActive !== false).map((t) => ({
795
+ id: t.id,
796
+ label: t.name,
797
+ transactionType: t.transactionType,
798
+ allowedAccountTypes: t.allowedAccountTypes
799
+ }));
800
+ }
801
+ var ACCOUNT_TYPES_FOR_TX = {
802
+ cash: ["cash_physical", "cash_session"],
803
+ credit_card: ["card_receivable_credit"],
804
+ debit_card: ["card_receivable_debit"],
805
+ pix: ["bank"],
806
+ bank_transfer: ["bank"],
807
+ boleto: ["bank"],
808
+ check: ["bank"],
809
+ voucher: ["bank"]
810
+ };
811
+ async function eligibleAccounts(provider, method) {
812
+ const accounts = (await provider.getBankAccounts().catch(() => [])).filter((a) => a.isActive !== false);
813
+ const allowed = method.allowedAccountTypes?.length ? method.allowedAccountTypes : ACCOUNT_TYPES_FOR_TX[method.transactionType ?? ""];
814
+ if (!allowed) return accounts;
815
+ return accounts.filter((a) => allowed.includes(a.accountType));
816
+ }
817
+ function matchPaymentOption(options, raw) {
818
+ const wanted = typeof raw === "string" ? raw.trim().toLowerCase() : "";
819
+ if (!wanted) return null;
820
+ return options.find((o) => o.id.toLowerCase() === wanted) ?? options.find((o) => o.label.toLowerCase() === wanted) ?? options.find((o) => o.label.toLowerCase().includes(wanted) || wanted.includes(o.label.toLowerCase())) ?? null;
821
+ }
822
+ async function settlePayable(provider, input) {
823
+ const schedule = await provider.getMovements({
824
+ invoiceId: input.invoiceId,
825
+ direction: "debit",
826
+ status: "pending"
827
+ });
828
+ const bill = schedule.data.find((m) => m.movementKind === "bill");
829
+ if (!bill) {
830
+ return {
831
+ ok: false,
832
+ error: "no_open_bill",
833
+ message: "That bill has no open installment \u2014 it may already be settled. Say so and stop."
834
+ };
835
+ }
836
+ const invoice = await provider.getInvoiceById(input.invoiceId).catch(() => null);
837
+ const money = (n) => formatCurrency(n, input.currency ?? "BRL");
838
+ const invoiceLabel = [
839
+ invoice?.fiscalNumber ? `#${invoice.fiscalNumber}` : null,
840
+ invoice?.contactName ?? null,
841
+ money(bill.amount)
842
+ ].filter(Boolean).join(" \u2014 ") || money(bill.amount);
843
+ const needsAccount = input.method.transactionType !== "cash";
844
+ const accounts = needsAccount ? await eligibleAccounts(provider, input.method) : [];
845
+ if (needsAccount && !accounts.length) {
846
+ return {
847
+ ok: false,
848
+ error: "no_eligible_account",
849
+ message: `The bill is recorded but NOT settled: this business has no account that can settle a ${input.method.label} payment. Say that, and that it has to be registered from the payables screen.`
850
+ };
851
+ }
852
+ const onlyAccount = accounts.length === 1 ? accounts[0] : void 0;
853
+ const paymentDate = parseReceiptDate(input.paymentDate) ?? today();
854
+ const decision = await useChatStore.getState().requestConfirmation({
855
+ id: `payment-${bill.id}`,
856
+ toolName: "registerPayablePayment",
857
+ title: "Registrar pagamento",
858
+ params: {
859
+ invoice: invoiceLabel,
860
+ method: input.method.label,
861
+ accountId: onlyAccount?.id,
862
+ paymentDate,
863
+ amount: bill.amount
864
+ },
865
+ plane: "client",
866
+ scope: "financial:payment",
867
+ scopeLabel: "Registrar pagamento",
868
+ fields: [
869
+ // Qual conta está sendo quitada, por extenso e primeiro. Só de leitura:
870
+ // é a identidade do que se assina, não um campo a editar.
871
+ { key: "invoice", label: "Conta a pagar", type: "text", readOnly: true },
872
+ { key: "method", label: "Forma de pagamento", type: "text", required: true, readOnly: true },
873
+ ...needsAccount ? [{
874
+ key: "accountId",
875
+ label: "Conta de sa\xEDda",
876
+ type: "select",
877
+ required: true,
878
+ options: accounts.map((a) => ({ value: a.id, label: a.name }))
879
+ }] : [],
880
+ { key: "paymentDate", label: "Data do pagamento", type: "date", required: true },
881
+ { key: "amount", label: "Valor pago", type: "number", required: true, min: 0 }
882
+ ]
883
+ });
884
+ if (!decision.approved) {
885
+ return {
886
+ ok: false,
887
+ cancelled: true,
888
+ reason: "user_declined",
889
+ message: "The bill is recorded but NOT settled \u2014 the payment step was declined. Say exactly that."
890
+ };
891
+ }
892
+ const values = { paymentDate, amount: bill.amount, accountId: onlyAccount?.id, ...decision.values ?? {} };
893
+ const accountId = typeof values.accountId === "string" && values.accountId ? values.accountId : void 0;
894
+ if (needsAccount && !accountId) {
895
+ return {
896
+ ok: false,
897
+ error: "no_account",
898
+ message: "NOTHING WAS SETTLED. The form came back with no outgoing account \u2014 do not say it was paid."
899
+ };
900
+ }
901
+ await provider.payMovement({
902
+ movementId: bill.id,
903
+ amount: parseReceiptAmount(values.amount) ?? bill.amount,
904
+ paymentDate: parseReceiptDate(values.paymentDate) ?? paymentDate,
905
+ paymentMethodTypeId: input.method.id,
906
+ bankAccountId: accountId
907
+ });
908
+ return {
909
+ ok: true,
910
+ settled: true,
911
+ invoiceId: input.invoiceId,
912
+ movementId: bill.id,
913
+ method: input.method.label,
914
+ account: accounts.find((a) => a.id === accountId)?.name,
915
+ paymentDate: parseReceiptDate(values.paymentDate) ?? paymentDate,
916
+ // O botão que abre a conta quitada. `path` existe para exatamente isto —
917
+ // uma tela de plugin que o mapa de rotas por arquétipo não sabe resolver.
918
+ ref: {
919
+ id: input.invoiceId,
920
+ label: invoiceLabel,
921
+ path: `/financial/payables/${input.invoiceId}`
922
+ },
923
+ message: `Pago via ${input.method.label}. Diga em UMA frase que a conta foi lan\xE7ada e quitada, e pare.`
924
+ };
925
+ }
926
+ var recentPayables = [];
927
+ var RECENT_WINDOW_MS = 10 * 60 * 1e3;
928
+ function rememberPayable(entry) {
929
+ recentPayables = [...recentPayables.filter((p) => p.invoiceId !== entry.invoiceId), entry].slice(-10);
930
+ }
931
+ function findRecentPayable(supplier, amount) {
932
+ const now = Date.now();
933
+ recentPayables = recentPayables.filter((p) => now - p.at < RECENT_WINDOW_MS);
934
+ const wanted = supplier.trim().toLowerCase();
935
+ return recentPayables.find(
936
+ (p) => p.supplier.trim().toLowerCase() === wanted && Math.abs(p.amount - amount) < 5e-3
937
+ );
938
+ }
939
+ function clearRecentPayables() {
940
+ recentPayables = [];
941
+ }
942
+ function asChoiceCards(choices) {
943
+ const usable = choices.filter((c) => c.options.length >= 2);
944
+ return usable.length ? { kind: "record_choice", choices: usable } : {};
945
+ }
946
+ function gapChoices(input) {
947
+ const questions = [];
948
+ const { supplierName, supplier, method, account } = input;
949
+ if (supplier.status !== "found" && supplierName) {
950
+ const candidates = supplier.candidates ?? [];
951
+ questions.push({
952
+ id: "supplier",
953
+ question: candidates.length ? `"${supplierName}" n\xE3o est\xE1 cadastrado. \xC9 algum destes?` : `"${supplierName}" ainda n\xE3o est\xE1 cadastrado. Cadastrar agora?`,
954
+ options: [
955
+ // Os parecidos PRIMEIRO: a razão de eles existirem é evitar o
956
+ // duplicado, então a opção de criar não pode ser a mais fácil de tocar.
957
+ ...candidates.slice(0, 4).map((c) => ({
958
+ id: `supplier:${c.id}`,
959
+ label: c.name,
960
+ meta: c.why
961
+ })),
962
+ { id: "supplier:create", label: `Criar fornecedor "${supplierName}"` },
963
+ { id: "supplier:cancel", label: "Cancelar" }
964
+ ]
965
+ });
966
+ }
967
+ if (method.status !== "found") {
968
+ const available = method.available ?? [];
969
+ questions.push({
970
+ id: "method",
971
+ question: method.spoken ? `O papel diz "${String(method.spoken)}", que n\xE3o est\xE1 cadastrado. Qual forma corresponde?` : "Como esta conta foi (ou vai ser) paga?",
972
+ options: [
973
+ ...available.slice(0, 5).map((o) => ({ id: `method:${o.id}`, label: o.label })),
974
+ { id: "method:none", label: "Deixar em aberto" }
975
+ ]
976
+ });
977
+ }
978
+ if (account.status === "ambiguous") {
979
+ const options = account.options ?? [];
980
+ questions.push({
981
+ id: "account",
982
+ question: account.cardLast4 ? `Nenhuma conta cadastrada tem o final ${String(account.cardLast4)}. De qual conta saiu?` : "De qual conta o dinheiro saiu?",
983
+ options: [
984
+ ...options.slice(0, 5).map((o) => ({ id: `account:${o.id}`, label: o.name })),
985
+ { id: "account:skip", label: "Decidir na hora de pagar" }
986
+ ]
987
+ });
988
+ }
989
+ return questions;
990
+ }
991
+ function askHowItWasPaid(invoiceId, options) {
992
+ return {
993
+ kind: "record_choice",
994
+ choices: [{
995
+ id: `payment-method-${invoiceId}`,
996
+ question: "Foi pago como?",
997
+ options
998
+ }],
999
+ invoiceId,
1000
+ nextStep: "registerPayablePayment",
1001
+ message: `The bill is recorded but NOT yet settled. The person is being asked HOW it was paid \u2014 the options are on screen. Do not list them again in prose and do not guess one. When they answer, call registerPayablePayment with invoiceId "${invoiceId}" and the option id they chose.`
1002
+ };
1003
+ }
1004
+ function createPayableTools({ config, provider }) {
1005
+ const lookup = config.contactLookup ?? createArchetypeLookup({
1006
+ archetype: "person",
1007
+ kind: "supplier",
1008
+ createKinds: ["supplier", "staff", "customer"],
1009
+ kindLabels: { supplier: "Fornecedor", customer: "Cliente", staff: "Funcion\xE1rio" }
1010
+ });
1011
+ return [
1012
+ {
1013
+ tool: RESOLVE_CONTEXT,
1014
+ execute: async (args) => {
1015
+ consumePayableDraft();
1016
+ const supplierName = text(args.supplier);
1017
+ const supplier = supplierName ? await findSupplier(lookup, supplierName, args.supplierDocument) : { status: "missing", candidates: [] };
1018
+ const method = await findMethod(provider, args);
1019
+ const account = await findAccount(provider, method, args.cardLast4);
1020
+ const gaps = [
1021
+ supplier.status !== "found" ? "supplier" : null,
1022
+ method.status !== "found" ? "method" : null,
1023
+ account.status === "ambiguous" || account.status === "missing" ? "account" : null
1024
+ ].filter(Boolean);
1025
+ const choices = gapChoices({ supplierName, supplier, method, account });
1026
+ const document = normalizeDocument(args);
1027
+ rememberDocument(document, {
1028
+ ...text(args.supplier) ? { source: text(args.supplier) } : {},
1029
+ ...useChatStore.getState().conversationId ? { conversationId: useChatStore.getState().conversationId } : {}
1030
+ });
1031
+ return {
1032
+ ok: true,
1033
+ read: {
1034
+ supplier: supplierName,
1035
+ supplierDocument: onlyDigits(args.supplierDocument) || void 0,
1036
+ paymentMethod: text(args.paymentMethod),
1037
+ cardBrand: text(args.cardBrand),
1038
+ cardLast4: lastFour(args.cardLast4) || void 0
1039
+ },
1040
+ supplier,
1041
+ method,
1042
+ account,
1043
+ gaps,
1044
+ document,
1045
+ ...asChoiceCards(choices),
1046
+ message: 'NOTHING WAS WRITTEN \u2014 this was a check. Tell the person the result as ONE short line per dependency, saying which already exist and which are missing (e.g. "Fornecedor Printi: ja existe - Pix: cadastrado - cartao final 4242: nao encontrei"). FIRST say what you READ, in one line, from `document`: how many items, the total, the method, the order ref, and the dates \u2014 and when `document.dueDate` is null say plainly there is no due date on the paper (\xE0 vista) and that the form will ask. If `document.reconciles` is false and there are items and a total, say the numbers do not add up and ask, instead of choosing one. The extraction is SAVED: when you call createPayable next, you may omit items/shipping/total/dates entirely \u2014 it reuses exactly what was read here. Do NOT re-read the document to fill them in again; re-reading from memory is where wrong numbers come from. ' + (choices.length ? "The open questions are ALREADY ON SCREEN as tappable buttons \u2014 do NOT repeat the options in prose and do NOT ask the person to type an answer. End your reply after the checklist and WAIT for the tap. When they answer, the option id says what to do: `supplier:create` means create the supplier under the name in `read.supplier` (call createPayable and its supplier step will confirm it), `supplier:<uuid>` means use that existing one, `method:<id>` is the payment method, `account:<id>` the outgoing account, and anything `:cancel` means stop. " : "Then continue with createPayable for the bill, and the payment. ") + "Never invent a value the paper did not state."
1047
+ };
1048
+ }
1049
+ },
1050
+ {
1051
+ tool: OPEN_FULL_FORM,
1052
+ execute: async () => {
1053
+ adminNavigateTo("/financial/payables/new");
1054
+ return { ok: true, opened: "/financial/payables/new" };
1055
+ }
1056
+ },
1057
+ {
1058
+ tool: CREATE_PAYABLE,
1059
+ execute: async (args) => {
1060
+ if (hasLivePayableDraft()) {
1061
+ return {
1062
+ ok: false,
1063
+ error: "draft_open",
1064
+ message: "NOTHING WAS WRITTEN, and no second bill was created. The payable form is OPEN ON SCREEN with the draft this conversation has been building \u2014 that draft IS this bill. Continue in it: use `fillPayableDraft` for what is missing and `submitPayableDraft` to save. Do not call createPayable again while that form is open."
1065
+ };
1066
+ }
1067
+ const remembered = rememberedDocument();
1068
+ if (args.amount === void 0 && remembered?.total !== void 0) {
1069
+ args = { ...args, amount: String(remembered.total) };
1070
+ }
1071
+ if (typeof args.amount !== "string") {
1072
+ return {
1073
+ ok: false,
1074
+ error: "amount_must_be_literal",
1075
+ message: 'Send `amount` as the TEXT that was written, punctuation included ("50,00", "1.234,56") \u2014 never as a number. A decimal comma is a thousands separator in English, so converting it yourself turns fifty into five thousand. Resend the literal.'
1076
+ };
1077
+ }
1078
+ const figures = parseReceiptAmount(args.amount);
1079
+ const spelled = parseAmountInWords(args.amountInWords);
1080
+ const amount = spelled ?? figures;
1081
+ if (amount === null) {
1082
+ return {
1083
+ ok: false,
1084
+ error: "invalid_amount",
1085
+ message: "That is not a readable amount. If the total is illegible or was never said, ask the person."
1086
+ };
1087
+ }
1088
+ const lines = args.items === void 0 && remembered?.items.length ? { items: remembered.items, error: void 0 } : parseLineItems(args.items);
1089
+ if (lines.error) return { ok: false, error: "invalid_items", message: lines.error };
1090
+ const shipping = args.shipping === void 0 ? remembered?.shipping ?? null : parseReceiptAmount(args.shipping);
1091
+ const lineSum = round2(lines.items.reduce((s, i) => s + i.quantity * i.unitPrice, 0));
1092
+ const computed = round2(lineSum + (shipping ?? 0));
1093
+ const candidates = [
1094
+ { value: amount, from: "argumento" },
1095
+ ...remembered?.total !== void 0 ? [{ value: remembered.total, from: "leitura" }] : []
1096
+ ];
1097
+ const agreed = lines.items.length ? candidates.find((c) => Math.abs(computed - c.value) <= 0.02) : candidates[0];
1098
+ if (lines.items.length && !agreed) {
1099
+ return {
1100
+ ok: false,
1101
+ error: "total_mismatch",
1102
+ lineSum,
1103
+ shipping: shipping ?? void 0,
1104
+ computed,
1105
+ total: amount,
1106
+ ...remembered?.total !== void 0 ? { totalFromDocument: remembered.total } : {},
1107
+ message: `NOTHING WAS WRITTEN. The lines do not add up to the total: the item(s) sum to ${lineSum}${shipping ? ` plus ${shipping} of shipping` : ""} = ${computed}, but the total given is ${amount}. Do NOT pick one of them and do not adjust a line to make it fit. Re-read the document \u2014 a line may have been missed, a quantity misread, or a discount or shipping line not captured. If the paper genuinely disagrees with itself, ask the person which is right.`
1108
+ };
1109
+ }
1110
+ const reconciledTotal = agreed?.value ?? amount;
1111
+ const correctedFromRead = agreed?.from === "leitura" && Math.abs(agreed.value - amount) > 5e-3;
1112
+ const dueDate = parseReceiptDate(args.dueDate) ?? remembered?.dueDate ?? void 0;
1113
+ const issueDate = parseReceiptDate(args.date) ?? remembered?.documentDate ?? parseReceiptDate(findDateIn(args.notes));
1114
+ const params = { amount };
1115
+ const supplierName = text(args.supplier);
1116
+ if (supplierName) params.supplier = supplierName;
1117
+ const description = text(args.description);
1118
+ if (description) params.description = description;
1119
+ if (dueDate) params.dueDate = dueDate;
1120
+ if (issueDate) params.issueDate = issueDate;
1121
+ const carried = [
1122
+ remembered?.orderRef ? `Pedido: ${remembered.orderRef}` : null,
1123
+ remembered?.deliveryDate ? `Entrega: ${remembered.deliveryDate}` : null
1124
+ ].filter(Boolean).join(" \xB7 ");
1125
+ const notes = text(args.notes) ?? (carried || void 0);
1126
+ if (notes) params.notes = notes;
1127
+ if (lines.items.length) {
1128
+ params.items = lines.items.map((i) => `${i.quantity}\xD7 ${i.description} @ ${i.unitPrice}`).join(" \xB7 ");
1129
+ params.amount = computed;
1130
+ }
1131
+ if (shipping) params.shipping = shipping;
1132
+ if (supplierName) {
1133
+ const already = findRecentPayable(supplierName, amount);
1134
+ if (already) {
1135
+ return {
1136
+ ok: false,
1137
+ error: "already_recorded",
1138
+ invoiceId: already.invoiceId,
1139
+ message: `NOTHING WAS WRITTEN and no second bill was created. The bill for ${supplierName} of ${amount} was ALREADY recorded in this conversation \u2014 its invoiceId is "${already.invoiceId}". If the person is saying it was PAID ("marca como pago", "paguei no Pix"), call registerPayablePayment with invoiceId "${already.invoiceId}". Only call createPayable again if this is genuinely a DIFFERENT bill, with a different amount or supplier.`
1140
+ };
1141
+ }
1142
+ }
1143
+ let supplier = null;
1144
+ if (supplierName) {
1145
+ const step = await resolveSupplier(lookup, supplierName);
1146
+ if (!step.ok) {
1147
+ return {
1148
+ ok: false,
1149
+ cancelled: true,
1150
+ reason: "user_declined_supplier",
1151
+ message: "NOTHING WAS WRITTEN. The person declined creating the supplier, so the bill has nobody to point at. Ask which existing supplier it belongs to \u2014 do not retry the same name."
1152
+ };
1153
+ }
1154
+ supplier = { id: step.id, name: step.name, created: step.created };
1155
+ params.supplier = step.name;
1156
+ }
1157
+ const decision = await useChatStore.getState().requestConfirmation({
1158
+ id: `payable-${Date.now()}`,
1159
+ toolName: "createPayable",
1160
+ // Nomeia o que está sendo criado. Era `config.labels.payablesNew`,
1161
+ // que é o rótulo da AÇÃO na sub-navegação ("New") — no topo de um
1162
+ // cartão de confirmação ele dizia só "New" e não nomeava nada.
1163
+ title: "Nova conta a pagar",
1164
+ params,
1165
+ plane: "client",
1166
+ scope: "financial:payable",
1167
+ scopeLabel: "Lan\xE7ar conta a pagar",
1168
+ fields: [
1169
+ { key: "supplier", label: "Fornecedor", type: "text", required: true },
1170
+ { key: "description", label: "Descri\xE7\xE3o", type: "text", required: true },
1171
+ // As linhas lidas da tabela, à vista e só de leitura: é o que a
1172
+ // pessoa confere para saber se a leitura do papel bateu.
1173
+ ...lines.items.length ? [{ key: "items", label: "Itens", type: "text", readOnly: true }] : [],
1174
+ ...shipping ? [{ key: "shipping", label: "Frete", type: "number", readOnly: true }] : [],
1175
+ // Com tabela lida, o valor É a soma conferida — editá-lo aqui o
1176
+ // separaria das linhas, e o banco recusa uma fatura cujas parcelas
1177
+ // não fecham com os itens. Sem tabela, ele continua editável.
1178
+ {
1179
+ key: "amount",
1180
+ label: "Valor total",
1181
+ type: "number",
1182
+ required: true,
1183
+ min: 0,
1184
+ ...lines.items.length ? { readOnly: true } : {}
1185
+ },
1186
+ { key: "dueDate", label: "Vencimento", type: "date", required: true },
1187
+ { key: "issueDate", label: "Data do documento", type: "date", default: today() },
1188
+ { key: "notes", label: "Observa\xE7\xF5es", type: "text" }
1189
+ ]
1190
+ });
1191
+ if (!decision.approved) return { ok: false, cancelled: true, reason: "user_declined" };
1192
+ const values = { ...params, ...decision.values ?? {} };
1193
+ const finalAmount = lines.items.length ? computed : parseReceiptAmount(values.amount) ?? amount;
1194
+ const finalSupplier = text(values.supplier);
1195
+ const finalDescription = text(values.description) ?? finalSupplier ?? "";
1196
+ const finalDue = parseReceiptDate(values.dueDate);
1197
+ const finalIssue = parseReceiptDate(values.issueDate) ?? today();
1198
+ if (!finalSupplier || !finalDue) {
1199
+ return {
1200
+ ok: false,
1201
+ error: "incomplete",
1202
+ message: "NOTHING WAS WRITTEN. The form came back without a supplier or a due date \u2014 do not tell the user it worked."
1203
+ };
1204
+ }
1205
+ let resolvedSupplier = supplier;
1206
+ if (!resolvedSupplier || resolvedSupplier.name !== finalSupplier) {
1207
+ const again = await resolveSupplier(lookup, finalSupplier);
1208
+ if (!again.ok) {
1209
+ return {
1210
+ ok: false,
1211
+ cancelled: true,
1212
+ reason: "user_declined_supplier",
1213
+ message: "NOTHING WAS WRITTEN. The supplier step was declined \u2014 ask which supplier this bill belongs to."
1214
+ };
1215
+ }
1216
+ resolvedSupplier = { id: again.id, name: again.name, created: again.created };
1217
+ }
1218
+ const invoice = await provider.createInvoice({
1219
+ direction: "debit",
1220
+ invoiceDate: finalIssue,
1221
+ contactId: resolvedSupplier.id,
1222
+ contactName: resolvedSupplier.name,
1223
+ observations: text(values.notes),
1224
+ // A nota que o papel descreve, linha por linha. Sem tabela, a conta
1225
+ // nasce com uma linha só — que é o que um recibo de táxi é. O FRETE é
1226
+ // linha PRÓPRIA: somado ao preço de um produto ele sumiria dentro
1227
+ // dele, e a conciliação com a transportadora não teria onde olhar.
1228
+ items: [
1229
+ ...lines.items.length ? lines.items.map((line) => ({
1230
+ itemKind: "other",
1231
+ description: line.description,
1232
+ quantity: line.quantity,
1233
+ unitPrice: line.unitPrice
1234
+ })) : [{
1235
+ itemKind: "other",
1236
+ description: finalDescription,
1237
+ quantity: 1,
1238
+ unitPrice: round2(finalAmount - (shipping ?? 0))
1239
+ }],
1240
+ ...shipping ? [{ itemKind: "other", description: "Frete", quantity: 1, unitPrice: shipping }] : []
1241
+ ],
1242
+ installments: [{
1243
+ direction: "debit",
1244
+ movementKind: "bill",
1245
+ amount: finalAmount,
1246
+ dueDate: finalDue,
1247
+ installmentNumber: 1
1248
+ }]
1249
+ });
1250
+ const id = invoice?.id;
1251
+ if (id) {
1252
+ rememberPayable({ invoiceId: id, supplier: resolvedSupplier.name, amount: finalAmount, at: Date.now() });
1253
+ }
1254
+ consumePayableDraft();
1255
+ const drift = spelled !== null && figures !== null && Math.abs(spelled - figures) > 5e-3 ? { note: `Os algarismos pareciam ${figures} e o extenso diz ${spelled}. Usei o extenso.` } : {};
1256
+ const invoicePath = id ? `/financial/payables/${id}` : null;
1257
+ const redirected = invoicePath !== null && args.redirect !== false;
1258
+ if (redirected) {
1259
+ adminNavigateTo(invoicePath);
1260
+ revealPage();
1261
+ }
1262
+ const invoiceLink = invoicePath ? { ref: { id, label: `${resolvedSupplier.name} \u2014 ${finalDescription}`, path: invoicePath } } : {};
1263
+ if (args.paid === true && id) {
1264
+ const options = await paymentOptions(provider);
1265
+ const chosen = matchPaymentOption(options, args.paymentMethod);
1266
+ if (!chosen) {
1267
+ if (options.length >= 2) {
1268
+ return {
1269
+ ...askHowItWasPaid(id, options),
1270
+ supplier: resolvedSupplier.name,
1271
+ supplierCreated: resolvedSupplier.created,
1272
+ amount: finalAmount,
1273
+ openedInvoice: redirected,
1274
+ ...drift
1275
+ };
1276
+ }
1277
+ return {
1278
+ ok: true,
1279
+ invoiceId: id,
1280
+ settled: false,
1281
+ supplier: resolvedSupplier.name,
1282
+ amount: finalAmount,
1283
+ dueDate: finalDue,
1284
+ openedInvoice: redirected,
1285
+ ...invoiceLink,
1286
+ message: "A conta foi lan\xE7ada, mas o pagamento N\xC3O foi registrado: este neg\xF3cio n\xE3o tem formas de pagamento cadastradas para escolher. Diga isso e ofere\xE7a registrar a baixa pela tela.",
1287
+ ...drift
1288
+ };
1289
+ }
1290
+ const settled = await settlePayable(provider, {
1291
+ invoiceId: id,
1292
+ method: chosen,
1293
+ paymentDate: typeof args.paymentDate === "string" ? args.paymentDate : void 0,
1294
+ currency: config.currency?.code
1295
+ });
1296
+ return {
1297
+ ...settled,
1298
+ invoiceId: id,
1299
+ supplier: resolvedSupplier.name,
1300
+ supplierCreated: resolvedSupplier.created,
1301
+ amount: finalAmount,
1302
+ openedInvoice: redirected,
1303
+ ...drift
1304
+ };
1305
+ }
1306
+ return {
1307
+ ok: true,
1308
+ invoiceId: id,
1309
+ settled: false,
1310
+ supplier: resolvedSupplier.name,
1311
+ supplierCreated: resolvedSupplier.created,
1312
+ amount: finalAmount,
1313
+ dueDate: finalDue,
1314
+ openedInvoice: redirected,
1315
+ ...lines.items.length ? { itemsWritten: lines.items.length } : {},
1316
+ // Dito, nunca silencioso: se o total que veio na chamada perdeu os
1317
+ // centavos e o da leitura fechou com as linhas, quem escreveu foi a
1318
+ // leitura — e quem confere merece saber disso.
1319
+ ...correctedFromRead ? {
1320
+ correctedTotal: {
1321
+ sent: amount,
1322
+ written: reconciledTotal,
1323
+ why: "o total enviado n\xE3o fechava com as linhas; o lido do documento fechou"
1324
+ }
1325
+ } : {},
1326
+ ...shipping ? { shipping } : {},
1327
+ // A CONTA é o destino principal — é o que a pessoa acabou de criar.
1328
+ ...invoiceLink,
1329
+ // O fornecedor vai como segundo botão, e só onde existe tela para ele
1330
+ // (ver supplierRef): `extractRecordLinks` lê `ref` e `record.ref`.
1331
+ ...resolvedSupplier.id ? { record: supplierRef(resolvedSupplier.id, resolvedSupplier.name) } : {},
1332
+ // O que fazer DEPOIS, dito com o id dentro. Sem esta linha, "marca
1333
+ // como pago no pix hoje" na mensagem seguinte virou uma SEGUNDA conta
1334
+ // a pagar: o modelo tinha a ferramenta de baixa e nenhuma frase
1335
+ // ligando-a a esta fatura.
1336
+ message: `A conta a pagar de ${resolvedSupplier.name} foi lan\xE7ada (invoiceId "${id}"), em aberto. ` + (resolvedSupplier.created ? "O fornecedor foi cadastrado agora. " : "") + (redirected ? "A tela da conta J\xC1 foi aberta \u2014 n\xE3o ofere\xE7a abri-la. " : "") + `Diga isso em UMA frase e pare \u2014 n\xE3o repita os campos, a pessoa acabou de conferi-los. IMPORTANTE: se a pessoa disser depois que esta conta foi paga ("marca como pago", "paguei no Pix", "foi pago hoje"), chame registerPayablePayment com invoiceId "${id}". N\xC3O chame createPayable de novo \u2014 isso criaria uma segunda conta para a mesma despesa.`,
1337
+ ...drift
1338
+ };
1339
+ }
1340
+ },
1341
+ {
1342
+ tool: REGISTER_PAYMENT,
1343
+ execute: async (args) => {
1344
+ const invoiceId = typeof args.invoiceId === "string" ? args.invoiceId : "";
1345
+ if (!invoiceId) {
1346
+ return {
1347
+ ok: false,
1348
+ error: "missing_invoice",
1349
+ message: "Which bill? Pass the `invoiceId` that createPayable returned."
1350
+ };
1351
+ }
1352
+ const options = await paymentOptions(provider);
1353
+ const chosen = matchPaymentOption(options, args.method);
1354
+ if (!chosen) {
1355
+ if (options.length < 2) {
1356
+ return {
1357
+ ok: false,
1358
+ error: "no_payment_methods",
1359
+ message: "This business has no payment methods to choose from. Say so instead of guessing one."
1360
+ };
1361
+ }
1362
+ return askHowItWasPaid(invoiceId, options);
1363
+ }
1364
+ return settlePayable(provider, {
1365
+ invoiceId,
1366
+ method: chosen,
1367
+ paymentDate: typeof args.paymentDate === "string" ? args.paymentDate : void 0,
1368
+ currency: config.currency?.code
1369
+ });
1370
+ }
1371
+ }
1372
+ ];
1373
+ }
1374
+ var liveDraft = null;
1375
+ function hasLivePayableDraft() {
1376
+ return liveDraft !== null;
1377
+ }
1378
+ var COMPANY_NOISE = /* @__PURE__ */ new Set([
1379
+ "ltda",
1380
+ "ltd",
1381
+ "me",
1382
+ "epp",
1383
+ "eireli",
1384
+ "sa",
1385
+ "mei",
1386
+ "cia",
1387
+ "ei",
1388
+ "inc",
1389
+ "comercio",
1390
+ "servicos",
1391
+ "industria",
1392
+ "do",
1393
+ "da",
1394
+ "de",
1395
+ "dos",
1396
+ "das",
1397
+ "e"
1398
+ ]);
1399
+ function normalizeCompanyName(raw) {
1400
+ if (typeof raw !== "string") return "";
1401
+ return raw.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w && !COMPANY_NOISE.has(w)).join(" ").trim();
1402
+ }
1403
+ function onlyDigits(raw) {
1404
+ return typeof raw === "string" ? raw.replace(/\D/g, "") : "";
1405
+ }
1406
+ function lastFour(raw) {
1407
+ const digits = onlyDigits(raw);
1408
+ return digits.length >= 4 ? digits.slice(-4) : "";
1409
+ }
1410
+ async function findSupplier(lookup, rawName, rawDocument) {
1411
+ const wanted = normalizeCompanyName(rawName);
1412
+ const doc = onlyDigits(rawDocument);
1413
+ if (!wanted) return { status: "missing", candidates: [] };
1414
+ const core = wanted.split(" ")[0] ?? wanted;
1415
+ const seen = /* @__PURE__ */ new Map();
1416
+ for (const query of core === wanted ? [core] : [core, wanted]) {
1417
+ for (const hit of await lookup.search(query).catch(() => [])) {
1418
+ const row = hit.data ?? {};
1419
+ seen.set(hit.id, {
1420
+ id: hit.id,
1421
+ name: hit.label,
1422
+ document: onlyDigits(row.document_number) || void 0,
1423
+ why: ""
1424
+ });
1425
+ }
1426
+ }
1427
+ const all = [...seen.values()];
1428
+ if (doc) {
1429
+ const byDoc = all.find((c) => c.document && c.document === doc);
1430
+ if (byDoc) return { status: "found", match: { ...byDoc, why: "mesmo CNPJ/CPF" }, candidates: [] };
1431
+ }
1432
+ const exact = all.filter((c) => normalizeCompanyName(c.name) === wanted);
1433
+ if (exact.length === 1) {
1434
+ const only = exact[0];
1435
+ if (doc && only.document && only.document !== doc) {
1436
+ return {
1437
+ status: "ambiguous",
1438
+ candidates: [{ ...only, why: "mesmo nome, mas o CNPJ do papel \xE9 outro" }]
1439
+ };
1440
+ }
1441
+ return { status: "found", match: { ...only, why: "nome id\xEAntico" }, candidates: [] };
1442
+ }
1443
+ const near = all.map((c) => {
1444
+ const n = normalizeCompanyName(c.name);
1445
+ if (!n) return null;
1446
+ if (n === wanted) return { ...c, why: "nome id\xEAntico" };
1447
+ if (n.startsWith(wanted) || wanted.startsWith(n)) return { ...c, why: "nome contido" };
1448
+ const shared = n.split(" ").filter((w) => wanted.split(" ").includes(w));
1449
+ return shared.length ? { ...c, why: `parte do nome em comum (${shared.join(" ")})` } : null;
1450
+ }).filter((c) => c !== null).slice(0, 5);
1451
+ return { status: near.length ? "ambiguous" : "missing", candidates: near };
1452
+ }
1453
+ async function findMethod(provider, args) {
1454
+ const types = await paymentOptions(provider);
1455
+ const spoken = [args.paymentMethod, args.cardBrand].filter((v) => typeof v === "string" && !!v.trim());
1456
+ const card = lastFour(args.cardLast4);
1457
+ const looksLikeCard = !!(card || args.cardBrand);
1458
+ let match = matchPaymentOption(types, args.paymentMethod);
1459
+ if (!match && looksLikeCard) {
1460
+ match = types.find((t) => t.transactionType === "credit_card") ?? null;
1461
+ }
1462
+ const configured = (await provider.getPaymentMethods().catch(() => [])).filter((m) => m.isActive !== false);
1463
+ const namedMethod = configured.find((m) => {
1464
+ const n = normalizeCompanyName(m.name);
1465
+ return !!n && spoken.some((s) => {
1466
+ const w = normalizeCompanyName(s);
1467
+ return !!w && (n === w || n.includes(w) || w.includes(n));
1468
+ });
1469
+ });
1470
+ if (!match) {
1471
+ return {
1472
+ status: "missing",
1473
+ spoken: spoken.join(" ") || void 0,
1474
+ cardLast4: card || void 0,
1475
+ available: types.map((t) => ({ id: t.id, label: t.label })),
1476
+ hint: "This business has no payment method matching what the paper says. ASK which of the available ones it maps to, or offer to add it in Settings (createRecord on financial:payment-methods is the right tool for THAT, and only that). Never silently pick one."
1477
+ };
1478
+ }
1479
+ return {
1480
+ status: "found",
1481
+ id: match.id,
1482
+ label: match.label,
1483
+ transactionType: match.transactionType,
1484
+ ...namedMethod ? { configuredMethod: { id: namedMethod.id, name: namedMethod.name } } : {},
1485
+ ...card ? { cardLast4: card } : {}
1486
+ };
1487
+ }
1488
+ async function findAccount(provider, method, rawLast4) {
1489
+ if (method.status !== "found") {
1490
+ return { status: "blocked", hint: "Resolve the payment method first \u2014 the eligible accounts depend on it." };
1491
+ }
1492
+ if (method.transactionType === "cash") {
1493
+ return {
1494
+ status: "not_applicable",
1495
+ hint: "Cash settles through the open cash session, not an account \u2014 nothing to choose."
1496
+ };
1497
+ }
1498
+ const accounts = await eligibleAccounts(provider, {
1499
+ id: String(method.id),
1500
+ label: String(method.label),
1501
+ transactionType: method.transactionType
1502
+ });
1503
+ const card = lastFour(rawLast4);
1504
+ const byCard = card ? accounts.find((a) => lastFour(a.accountNumber) === card || a.name.includes(card)) : void 0;
1505
+ if (byCard) return { status: "found", id: byCard.id, name: byCard.name, why: `final ${card}` };
1506
+ if (!accounts.length) {
1507
+ return {
1508
+ status: "missing",
1509
+ hint: `No account can settle a ${String(method.label)} payment. Say so \u2014 it has to be created in Settings.`
1510
+ };
1511
+ }
1512
+ if (card) {
1513
+ return {
1514
+ status: "ambiguous",
1515
+ cardLast4: card,
1516
+ note: `o papel diz final ${card} e nenhuma conta cadastrada tem esse final`,
1517
+ options: accounts.map((a) => ({ id: a.id, name: a.name })),
1518
+ hint: `ASK whether the card ending ${card} is one of these accounts, or whether it still has to be registered. Do not assume it is the only eligible one.`
1519
+ };
1520
+ }
1521
+ if (accounts.length === 1) {
1522
+ return { status: "found", id: accounts[0].id, name: accounts[0].name, why: "\xFAnica conta eleg\xEDvel" };
1523
+ }
1524
+ return {
1525
+ status: "ambiguous",
1526
+ ...card ? { cardLast4: card, note: `nenhuma conta bate com o final ${card}` } : {},
1527
+ options: accounts.map((a) => ({ id: a.id, name: a.name })),
1528
+ hint: "Ask which account the money left. Do not pick one."
1529
+ };
1530
+ }
1531
+ var RESOLVE_CONTEXT = {
1532
+ id: "financial.resolve-payable-context",
1533
+ name: "resolvePayableContext",
1534
+ description: "STEP 0, BEFORE writing anything: checks what a receipt names against what this business actually has. Call it as soon as you have read a receipt/photo/PDF, with EVERYTHING the paper says \u2014 supplier, its CNPJ/CPF if printed, the payment method, the card brand and last 4 digits if printed. Returns, for each dependency, whether it already exists here (fuzzy-matched on name and CNPJ, so the same supplier is never created twice), is ambiguous, or is missing \u2014 plus the candidates to choose from. Writes NOTHING. Report the result as a short checklist and then resolve the gaps IN ORDER: supplier first, then the payment method, then the bill (createPayable), then the payment.",
1535
+ icon: "ListChecks",
1536
+ mode: "read",
1537
+ execution: { plane: "client" },
1538
+ category: "Finance",
1539
+ role: "finance",
1540
+ permission: { feature: "financial", action: "read" },
1541
+ parameters: {
1542
+ type: "object",
1543
+ properties: {
1544
+ supplier: { type: "string", description: "O nome do fornecedor EXATAMENTE como est\xE1 escrito no papel." },
1545
+ supplierDocument: { type: "string", description: "O CNPJ ou CPF impresso, se houver. Copie os d\xEDgitos." },
1546
+ paymentMethod: {
1547
+ type: "string",
1548
+ description: 'Como o papel diz que foi pago ("PIX", "cart\xE3o de cr\xE9dito", "dinheiro", "boleto").'
1549
+ },
1550
+ cardBrand: { type: "string", description: 'A bandeira, se impressa ("Visa", "Mastercard", "Nubank").' },
1551
+ cardLast4: { type: "string", description: 'Os quatro \xFAltimos d\xEDgitos do cart\xE3o, se impressos ("4242").' },
1552
+ // O DOCUMENTO INTEIRO, lido UMA VEZ.
1553
+ //
1554
+ // Não é zelo: é onde a fabricação entrava. Extrair no passo 0 e extrair de
1555
+ // novo no passo da conta são duas leituras do mesmo papel, e a segunda não
1556
+ // tem o papel na frente — ela tem a memória da primeira, que é onde os
1557
+ // números começam a escorregar. Lido aqui, normalizado aqui, conferido
1558
+ // aqui, e as ferramentas seguintes CONSOMEM o que ficou guardado.
1559
+ items: {
1560
+ type: "array",
1561
+ description: "As LINHAS da tabela do documento, uma por linha \u2014 leia linha a linha e copie TODAS.",
1562
+ items: {
1563
+ type: "object",
1564
+ properties: {
1565
+ description: { type: "string", description: "O que \xE9 a linha, como est\xE1 escrito." },
1566
+ quantity: { type: "number", description: "A quantidade. 1 quando a tabela n\xE3o diz." },
1567
+ unitPrice: { type: "string", description: 'Pre\xE7o UNIT\xC1RIO copiado ("47,90").' }
1568
+ },
1569
+ required: ["description", "unitPrice"]
1570
+ }
1571
+ },
1572
+ shipping: { type: "string", description: 'O FRETE cobrado \xE0 parte, copiado ("52,19").' },
1573
+ subtotal: { type: "string", description: 'O subtotal impresso, se houver ("1.437,00").' },
1574
+ total: {
1575
+ type: "string",
1576
+ description: 'O TOTAL do rodap\xE9, copiado letra por letra ("1.489,19"). Com frete, sem converter.'
1577
+ },
1578
+ documentDate: { type: "string", description: 'A data do documento ("09/09/2026").' },
1579
+ dueDate: {
1580
+ type: "string",
1581
+ description: 'O VENCIMENTO, s\xF3 se o papel disser. "Expira em" de um c\xF3digo Pix \xE9 a validade do C\xD3DIGO, n\xE3o vencimento: um pedido pago \xE0 vista n\xE3o tem, e a\xED se omite.'
1582
+ },
1583
+ paymentDate: { type: "string", description: "Quando foi pago, se o papel disser." },
1584
+ orderRef: { type: "string", description: 'O n\xFAmero do pedido ou documento ("#164223").' },
1585
+ deliveryDate: { type: "string", description: 'A data de entrega, se impressa ("11/09/2026").' }
1586
+ }
1587
+ }
1588
+ };
1589
+ var FILL_DRAFT = {
1590
+ id: "financial.fill-payable-draft",
1591
+ name: "fillPayableDraft",
1592
+ description: "Fills a field of the payable form ALREADY OPEN on screen (the draft the person is looking at). Use this \u2014 not createPayable \u2014 whenever that form is open: it is the bill in progress, and writing a new one instead would leave the person staring at a draft that means nothing. Looks the supplier up by name and offers to create it when it does not exist yet. Saves nothing.",
1593
+ icon: "PenLine",
1594
+ mode: "read",
1595
+ execution: { plane: "client" },
1596
+ category: "Finance",
1597
+ role: "finance",
1598
+ parameters: {
1599
+ type: "object",
1600
+ properties: {
1601
+ supplier: { type: "string", description: "O nome do fornecedor a colocar no rascunho." },
1602
+ dueDate: { type: "string", description: 'O vencimento, como foi dito ("20/01/2026").' }
1603
+ }
1604
+ }
1605
+ };
1606
+ var SUBMIT_DRAFT = {
1607
+ id: "financial.submit-payable-draft",
1608
+ name: "submitPayableDraft",
1609
+ description: `SAVES the payable form open on screen \u2014 the same thing the screen's save button does. This is how "salva", "pode salvar", "confirma" is answered while that form is open. Never call createPayable for a draft that is already on screen: that writes a second, duplicate bill.`,
1610
+ icon: "Check",
1611
+ mode: "persist",
1612
+ execution: { plane: "client" },
1613
+ requiresConfirmation: false,
1614
+ category: "Finance",
1615
+ role: "finance",
1616
+ permission: { feature: "financial", action: "create" },
1617
+ parameters: { type: "object", properties: {} }
1618
+ };
1619
+ function PayableDraftSurface({ config, draft }) {
1620
+ const live = React.useRef(draft);
1621
+ live.current = draft;
1622
+ const lookup = React.useMemo(
1623
+ () => config.contactLookup ?? createArchetypeLookup({
1624
+ archetype: "person",
1625
+ kind: "supplier",
1626
+ createKinds: ["supplier", "staff", "customer"],
1627
+ kindLabels: { supplier: "Fornecedor", customer: "Cliente", staff: "Funcion\xE1rio" }
1628
+ }),
1629
+ [config.contactLookup]
1630
+ );
1631
+ React.useEffect(() => {
1632
+ liveDraft = {
1633
+ describe: () => live.current.describe(),
1634
+ setSupplier: (name, id) => live.current.setSupplier(name, id),
1635
+ setDueDate: (iso) => live.current.setDueDate(iso),
1636
+ submit: () => live.current.submit()
1637
+ };
1638
+ return () => {
1639
+ liveDraft = null;
1640
+ };
1641
+ }, []);
1642
+ const contribution = React.useMemo(() => ({
1643
+ id: "financial.payables.draft",
1644
+ pluginId: "financial",
1645
+ role: "finance",
1646
+ instructions: 'O cadastro de uma CONTA A PAGAR est\xE1 aberto na tela, com um rascunho que a pessoa est\xE1 conferindo. Enquanto ele estiver a\xED, ele \xC9 a conta em curso: use `fillPayableDraft` para completar o que falta e `submitPayableDraft` para salvar. N\xC3O chame `createPayable` \u2014 ela criaria uma segunda conta e deixaria a pessoa num rascunho \xF3rf\xE3o. O estado do rascunho est\xE1 em "Tela aberta agora".',
1647
+ describe: () => {
1648
+ const state = live.current.describe();
1649
+ return {
1650
+ title: "Cadastro \u2014 Conta a pagar (rascunho n\xE3o salvo)",
1651
+ state: { screen: "/financial/payables/new", draft: state }
1652
+ };
1653
+ },
1654
+ tools: [
1655
+ {
1656
+ tool: FILL_DRAFT,
1657
+ execute: async (args) => {
1658
+ const filled = [];
1659
+ const dueDate = parseReceiptDate(args.dueDate);
1660
+ if (dueDate) {
1661
+ live.current.setDueDate(dueDate);
1662
+ filled.push("dueDate");
1663
+ }
1664
+ const name = text(args.supplier);
1665
+ if (name) {
1666
+ const step = await resolveSupplier(lookup, name);
1667
+ if (!step.ok) {
1668
+ return {
1669
+ ok: false,
1670
+ cancelled: true,
1671
+ reason: "user_declined_supplier",
1672
+ message: "The supplier was not created, so the draft still has none. Ask which supplier it is."
1673
+ };
1674
+ }
1675
+ live.current.setSupplier(step.name, step.id);
1676
+ filled.push("supplier");
1677
+ }
1678
+ if (!filled.length) {
1679
+ return { ok: false, error: "nothing_to_fill", message: "Send a supplier or a dueDate to fill in." };
1680
+ }
1681
+ return {
1682
+ ok: true,
1683
+ filled,
1684
+ draft: live.current.describe(),
1685
+ message: "O rascunho na tela foi preenchido \u2014 nada foi salvo ainda. Diga em UMA frase o que entrou e que basta confirmar. Quando a pessoa mandar salvar, chame submitPayableDraft."
1686
+ };
1687
+ }
1688
+ },
1689
+ {
1690
+ tool: SUBMIT_DRAFT,
1691
+ execute: async () => {
1692
+ const before = live.current.describe();
1693
+ if (!before.itemCount) {
1694
+ return {
1695
+ ok: false,
1696
+ error: "empty_draft",
1697
+ message: "NOTHING WAS SAVED. The draft has no line to bill. Say what is missing."
1698
+ };
1699
+ }
1700
+ await live.current.submit();
1701
+ return {
1702
+ ok: true,
1703
+ saved: true,
1704
+ supplier: before.supplier,
1705
+ total: before.total,
1706
+ message: "A conta a pagar foi salva a partir do rascunho que estava na tela, e a ficha dela j\xE1 abriu. Diga isso em UMA frase e pare."
1707
+ };
1708
+ }
1709
+ }
1710
+ ]
1711
+ }), [lookup]);
1712
+ useAgentSurface(contribution);
1713
+ return null;
1714
+ }
1715
+ function createFinancialAgentMount(deps) {
1716
+ const FinancialAgentMount = () => {
1717
+ const contribution = React.useMemo(() => ({
1718
+ id: "financial.payables.global",
1719
+ pluginId: "financial",
1720
+ role: "finance",
1721
+ instructions: PAYABLE_INSTRUCTIONS,
1722
+ // Sem `describe`: esta superfície não é uma tela. O snapshot responde "o
1723
+ // que está na tela agora", e um mount de shell que dissesse "Contas a
1724
+ // pagar" mentiria em todas as outras rotas do app.
1725
+ tools: createPayableTools(deps)
1726
+ }), []);
1727
+ useAgentSurface(contribution);
1728
+ return null;
1729
+ };
1730
+ FinancialAgentMount.displayName = "FinancialAgentMount";
1731
+ return FinancialAgentMount;
1732
+ }
1733
+
1734
+ export { PAYABLE_INSTRUCTIONS, PayableDraftSurface, RECEIPT_INSTRUCTIONS, RECEIVABLE_RECEIPT_INSTRUCTIONS, clearRecentPayables, consumePayableDraft, createFinancialAgentMount, createPayableTools, createReceiptTools, hasLivePayableDraft, normalizeCompanyName, startReceiptCapture };
1735
+ //# sourceMappingURL=chunk-7VKJLBYY.js.map
1736
+ //# sourceMappingURL=chunk-7VKJLBYY.js.map