next-finance-mcp 0.9.76 → 0.9.78

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/client.d.ts CHANGED
@@ -1019,6 +1019,42 @@ export interface ContatoDetalhado {
1019
1019
  valor?: number;
1020
1020
  }[];
1021
1021
  }
1022
+ /** Lista contratos da carteira (opcionalmente por pessoa). */
1023
+ export declare function listarContratos(o?: {
1024
+ pessoa?: string;
1025
+ pagina?: number;
1026
+ limite?: number;
1027
+ }): Promise<{
1028
+ contratos: Record<string, unknown>[];
1029
+ pagina: number;
1030
+ }>;
1031
+ /** Detalha um contrato (valor, reajuste, parcelas, índice detectado no texto). */
1032
+ export declare function detalharContrato(o: {
1033
+ numero?: number | string;
1034
+ pessoa?: string;
1035
+ }): Promise<Record<string, unknown>>;
1036
+ /** Calcula o reajuste (sem gravar): índice acumulado 12m (BCB) × valor atual. */
1037
+ export declare function calcularReajusteContrato(o: {
1038
+ numero?: number | string;
1039
+ pessoa?: string;
1040
+ indice?: string;
1041
+ meses?: number;
1042
+ }): Promise<Record<string, unknown>>;
1043
+ /** Aplica o reajuste no contrato (PUT), acrescenta a nota no texto e, por padrão,
1044
+ * gera as parcelas (GerarFaturamento). Retorna o antes/depois. */
1045
+ export declare function reajustarContrato(o: {
1046
+ numero?: number | string;
1047
+ pessoa?: string;
1048
+ indice?: string;
1049
+ meses?: number;
1050
+ responsavel?: string;
1051
+ gerarParcelas?: boolean;
1052
+ }): Promise<Record<string, unknown>>;
1053
+ /** Gera (fatura) as parcelas do contrato — GerarFaturamento. */
1054
+ export declare function gerarParcelasContrato(o: {
1055
+ numero?: number | string;
1056
+ pessoa?: string;
1057
+ }): Promise<Record<string, unknown>>;
1022
1058
  /** Invalida o cache de pessoas (chamar após criar/editar contato). */
1023
1059
  export declare function invalidarCachePessoas(): void;
1024
1060
  export type ResultadoBuscaContato = {
package/dist/client.js CHANGED
@@ -410,6 +410,45 @@ async function apiPost(path, body, retry = true) {
410
410
  return text;
411
411
  }
412
412
  }
413
+ /** PUT JSON para a API REST — mesma máquina de auth/refresh do apiPost. */
414
+ async function apiPut(path, body, retry = true) {
415
+ const auth = getAuthHeader();
416
+ const { statusCode, headers, body: resBody } = await reqWithTimeout(`${API_BASE}${path}`, {
417
+ method: "PUT",
418
+ headers: {
419
+ ...BASE_HEADERS, ...NEXT_BUSINESS_HEADERS, ...getApiKeyHeaders(),
420
+ Cookie: getCookieHeader("finance.net.br"),
421
+ "Content-Type": "application/json", Accept: "application/json",
422
+ ...(auth ? { Authorization: auth } : {}),
423
+ },
424
+ body: JSON.stringify(body),
425
+ dispatcher: agentFollow,
426
+ });
427
+ const setCookies = headers["set-cookie"];
428
+ storeCookies(Array.isArray(setCookies) ? setCookies : setCookies ? [setCookies] : [], "finance.net.br");
429
+ const text = await resBody.text();
430
+ if (isSessionExpired(statusCode, text)) {
431
+ if (retry && await recoverSession())
432
+ return apiPut(path, body, false);
433
+ throwSessionExpired();
434
+ }
435
+ if (retry && isCarteiraTokenExpired(statusCode, text)) {
436
+ if (await recoverSession())
437
+ return apiPut(path, body, false);
438
+ throw new Error("Não foi possível renovar o token da carteira no momento. Tente novamente em instantes.");
439
+ }
440
+ if (statusCode >= 400)
441
+ throw new Error(extrairMensagemApi(text) ?? `API PUT ${path}: ${statusCode}`);
442
+ try {
443
+ return JSON.parse(text);
444
+ }
445
+ catch {
446
+ const msg = extrairMensagemApi(text);
447
+ if (msg)
448
+ throw new Error(msg);
449
+ return text;
450
+ }
451
+ }
413
452
  function extractCsrf(html) {
414
453
  return html.match(/name="__RequestVerificationToken"[^>]+value="([^"]+)"/)?.[1] ?? "";
415
454
  }
@@ -1016,7 +1055,7 @@ async function resolverIdPlanoDeContas(nome, contexto) {
1016
1055
  // Usa a lista COMPLETA (GET /api/PlanoDeContas, cacheada) — inclui despesa, receita
1017
1056
  // E transferência. O /Filtrar retorna só parte (ex: só receitas em algumas carteiras),
1018
1057
  // o que escondia planos de despesa como "Semanadas e Mesadas".
1019
- const termo = nome.trim().toLowerCase().replace(/^\[|\]$/g, "");
1058
+ const termo = normalizar(nome.trim().replace(/^\[|\]$/g, "")); // sem acento/caixa
1020
1059
  const lista = await fetchPlanoDeContasCompleto();
1021
1060
  const nomeDe = nomePlano;
1022
1061
  const tipoDe = (p) => String(p["NomeTipoPlanoDeContas"] ?? "");
@@ -1031,13 +1070,13 @@ async function resolverIdPlanoDeContas(nome, contexto) {
1031
1070
  return t === "Despesa";
1032
1071
  return false;
1033
1072
  };
1034
- const matches = lista.filter(p => p["Ativo"] !== false && nomeDe(p).toLowerCase().includes(termo));
1073
+ const matches = lista.filter(p => p["Ativo"] !== false && normalizar(nomeDe(p)).includes(termo));
1035
1074
  if (matches.length === 0) {
1036
1075
  throw new Error(`Plano de contas "${nome}" não encontrado (nem categoria, nem transferência entre contas).`);
1037
1076
  }
1038
1077
  const compat = matches.filter(p => !incompativel(p));
1039
1078
  // Match exato (inclui plano de transferência "[Conta]") de tipo compatível.
1040
- const exata = compat.find(p => nomeDe(p).toLowerCase() === termo);
1079
+ const exata = compat.find(p => normalizar(nomeDe(p)) === termo);
1041
1080
  if (exata)
1042
1081
  return String(exata["Id"] ?? "");
1043
1082
  // Entre os compatíveis: categorias têm prioridade sobre transferência.
@@ -1123,11 +1162,11 @@ async function resolverPlanoTransferencia(nomeContaDestino) {
1123
1162
  async function resolverIdCentroDeCustos(nome) {
1124
1163
  const raw = await apiGet("/api/CentroDeCustos");
1125
1164
  const arr = Array.isArray(raw) ? raw : (Object.values(raw).find(v => Array.isArray(v)) ?? []);
1126
- const termo = nome.trim().toLowerCase();
1127
- const matches = arr.filter(c => String(c["Nome"] ?? c["Descricao"] ?? "").toLowerCase().includes(termo));
1165
+ const termo = normalizar(nome.trim()); // sem acento/caixa
1166
+ const matches = arr.filter(c => normalizar(String(c["Nome"] ?? c["Descricao"] ?? "")).includes(termo));
1128
1167
  if (matches.length === 0)
1129
1168
  throw new Error(`Centro de custo "${nome}" não encontrado.`);
1130
- const exata = matches.find(c => String(c["Nome"] ?? c["Descricao"] ?? "").toLowerCase() === termo);
1169
+ const exata = matches.find(c => normalizar(String(c["Nome"] ?? c["Descricao"] ?? "")) === termo);
1131
1170
  const escolhido = exata ?? (matches.length === 1 ? matches[0] : null);
1132
1171
  if (!escolhido) {
1133
1172
  const nomes = matches.slice(0, 10).map(c => String(c["Nome"] ?? c["Descricao"] ?? "")).join(", ");
@@ -3871,6 +3910,212 @@ async function contratosDaPessoa(idPessoa) {
3871
3910
  return [];
3872
3911
  }
3873
3912
  }
3913
+ // ── Contratos (NEXT Business) — reajuste por índice + parcelas ────────────────
3914
+ // Índice de reajuste NÃO é campo do contrato no NEXT: fica no texto de
3915
+ // `InformacaoAdicional` ("Informações sobre o contrato"). O valor do índice vem
3916
+ // da API pública do Banco Central (SGS): IPCA=433, IGP-M=189.
3917
+ const SGS_SERIE = { IPCA: 433, IGPM: 189 };
3918
+ const MESES_PT = ["Janeiro", "Fevereiro", "Março", "Abril", "Maio", "Junho", "Julho", "Agosto", "Setembro", "Outubro", "Novembro", "Dezembro"];
3919
+ const arred2 = (n) => Math.round(n * 100) / 100;
3920
+ const pctBR = (n) => n.toFixed(2).replace(".", ",");
3921
+ const dataBR = (d) => `${String(d.getDate()).padStart(2, "0")}/${String(d.getMonth() + 1).padStart(2, "0")}/${d.getFullYear()}`;
3922
+ /** Normaliza rótulo de índice para IPCA | IGPM. */
3923
+ function normalizarIndice(s) {
3924
+ const t = normalizar(s ?? "");
3925
+ if (/igp[\s-]?m|igpm/.test(t))
3926
+ return "IGPM";
3927
+ if (/ipca/.test(t))
3928
+ return "IPCA";
3929
+ return null;
3930
+ }
3931
+ /** Detecta o índice de reajuste no texto de "Informações sobre o contrato". */
3932
+ function detectarIndiceReajuste(texto) {
3933
+ return normalizarIndice(texto);
3934
+ }
3935
+ /** Índice acumulado dos últimos `meses` (default 12) até o último mês fechado
3936
+ * antes de `ateData` (default hoje). Fonte: BCB SGS (público, sem chave). */
3937
+ async function indiceReajusteAcumulado(indice, meses = 12, ateData) {
3938
+ const serie = SGS_SERIE[indice];
3939
+ if (!serie)
3940
+ throw new Error(`Índice "${indice}" não suportado (use IPCA ou IGPM).`);
3941
+ const ref = ateData ?? new Date();
3942
+ // Último mês fechado = mês anterior ao de referência. Puxa uma janela maior e pega os últimos N disponíveis.
3943
+ const fimRef = new Date(ref.getFullYear(), ref.getMonth() - 1, 1);
3944
+ const ini = new Date(fimRef.getFullYear(), fimRef.getMonth() - (meses + 2), 1);
3945
+ const dd = (d, last = false) => {
3946
+ const dia = last ? new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate() : 1;
3947
+ return `${String(dia).padStart(2, "0")}/${String(d.getMonth() + 1).padStart(2, "0")}/${d.getFullYear()}`;
3948
+ };
3949
+ const url = `https://api.bcb.gov.br/dados/serie/bcdata.sgs.${serie}/dados?formato=json&dataInicial=${dd(ini)}&dataFinal=${dd(fimRef, true)}`;
3950
+ const ctrl = new AbortController();
3951
+ const t = setTimeout(() => ctrl.abort(), 20000);
3952
+ let arr;
3953
+ try {
3954
+ const resp = await fetch(url, { signal: ctrl.signal });
3955
+ if (!resp.ok)
3956
+ throw new Error(`Banco Central respondeu HTTP ${resp.status}`);
3957
+ arr = await resp.json();
3958
+ }
3959
+ catch (e) {
3960
+ throw new Error(`Não consegui consultar o ${indice} no Banco Central agora (${e?.message ?? e}). Tente de novo em instantes.`);
3961
+ }
3962
+ finally {
3963
+ clearTimeout(t);
3964
+ }
3965
+ if (!Array.isArray(arr) || arr.length === 0)
3966
+ throw new Error(`O Banco Central não retornou dados de ${indice} para o período.`);
3967
+ const pts = arr.slice(-meses); // últimos N meses disponíveis (fechados)
3968
+ let fator = 1;
3969
+ const series = pts.map(x => { const v = parseFloat(x.valor); fator *= (1 + v / 100); return { mes: x.data.slice(3), valor: v }; });
3970
+ const [mFim, aFim] = series[series.length - 1].mes.split("/");
3971
+ return {
3972
+ indice, percentual: (fator - 1) * 100, fator,
3973
+ mesInicial: series[0].mes, mesFinal: series[series.length - 1].mes,
3974
+ mesFinalLabel: `${MESES_PT[Number(mFim) - 1]}/${aFim}`,
3975
+ series,
3976
+ };
3977
+ }
3978
+ /** Localiza um contrato pelo número (e opcionalmente pessoa). Retorna o objeto COMPLETO do GET. */
3979
+ async function resolverContratoCompleto(o) {
3980
+ const numero = o.numero != null && String(o.numero).trim() !== "" ? Number(String(o.numero).replace(/\D/g, "")) : undefined;
3981
+ let idPessoa;
3982
+ if (o.pessoa) {
3983
+ try {
3984
+ idPessoa = String((await resolverPessoa(o.pessoa))["Id"] ?? "");
3985
+ }
3986
+ catch { /* ignora */ }
3987
+ }
3988
+ // Pagina o Filtrar até achar o número (ou lista a 1ª página se só a pessoa foi dada).
3989
+ let achado;
3990
+ for (let pagina = 1; pagina <= 20 && !achado; pagina++) {
3991
+ const raw = await apiPost("/api/Contrato/Filtrar", { NumeroPagina: pagina, TamanhoPagina: 50, Nome: o.pessoa ?? "", PessoaId: idPessoa ?? null });
3992
+ const arr = (Array.isArray(raw) ? raw : Object.values(raw).find(v => Array.isArray(v)) ?? []);
3993
+ if (arr.length === 0)
3994
+ break;
3995
+ if (numero != null)
3996
+ achado = arr.find(c => Number(c["Numero"]) === numero);
3997
+ else if (arr.length === 1)
3998
+ achado = arr[0];
3999
+ if (arr.length < 50)
4000
+ break;
4001
+ }
4002
+ if (!achado)
4003
+ throw new Error(numero != null ? `Contrato número ${numero} não encontrado${o.pessoa ? ` para "${o.pessoa}"` : ""}.` : `Informe o número do contrato (não consegui identificar um único).`);
4004
+ const id = String(achado["Id"] ?? "");
4005
+ return await apiGet(`/api/Contrato/${encodeURIComponent(id)}`);
4006
+ }
4007
+ /** Versão exibível de um contrato (sem IDs internos). */
4008
+ function slimContrato(c) {
4009
+ return {
4010
+ numero: c["Numero"] != null ? Number(c["Numero"]) : undefined,
4011
+ pessoa: c["PessoaNome"] ? String(c["PessoaNome"]).trim() : undefined,
4012
+ valor: c["Valor"] != null ? Number(c["Valor"]) : undefined,
4013
+ situacao: c["Situacao"] ? String(c["Situacao"]).trim() : undefined,
4014
+ inicio: c["DataInicio"] ? String(c["DataInicio"]).slice(0, 10) : undefined,
4015
+ fim: c["DataFim"] ? String(c["DataFim"]).slice(0, 10) : undefined,
4016
+ reajuste_periodo: c["Reajuste"] != null ? `${c["Reajuste"]} ${c["ReajustePeriodo"] ?? ""}`.trim() : undefined,
4017
+ reajuste_status: c["ReajusteStatus"] ? String(c["ReajusteStatus"]).trim() : undefined,
4018
+ parcela_periodo: c["Parcela"] != null ? `${c["Parcela"]} ${c["ParcelaPeriodo"] ?? ""}`.trim() : undefined,
4019
+ indice_detectado: detectarIndiceReajuste(c["InformacaoAdicional"]) ?? undefined,
4020
+ };
4021
+ }
4022
+ /** Lista contratos da carteira (opcionalmente por pessoa). */
4023
+ export async function listarContratos(o = {}) {
4024
+ const pagina = o.pagina ?? 1;
4025
+ let idPessoa;
4026
+ if (o.pessoa) {
4027
+ try {
4028
+ idPessoa = String((await resolverPessoa(o.pessoa))["Id"] ?? "");
4029
+ }
4030
+ catch { /* ignora */ }
4031
+ }
4032
+ const raw = await apiPost("/api/Contrato/Filtrar", { NumeroPagina: pagina, TamanhoPagina: o.limite ?? 50, Nome: o.pessoa ?? "", PessoaId: idPessoa ?? null });
4033
+ const arr = (Array.isArray(raw) ? raw : Object.values(raw).find(v => Array.isArray(v)) ?? []);
4034
+ return { contratos: arr.map(slimContrato), pagina };
4035
+ }
4036
+ /** Detalha um contrato (valor, reajuste, parcelas, índice detectado no texto). */
4037
+ export async function detalharContrato(o) {
4038
+ const c = await resolverContratoCompleto(o);
4039
+ const slim = slimContrato(c);
4040
+ slim["informacoes"] = c["InformacaoAdicional"] ? String(c["InformacaoAdicional"]).trim() : undefined;
4041
+ return slim;
4042
+ }
4043
+ /** Calcula o reajuste (sem gravar): índice acumulado 12m (BCB) × valor atual. */
4044
+ export async function calcularReajusteContrato(o) {
4045
+ const c = await resolverContratoCompleto(o);
4046
+ const indice = normalizarIndice(o.indice) ?? detectarIndiceReajuste(c["InformacaoAdicional"]);
4047
+ if (!indice) {
4048
+ throw new Error(`Não identifiquei o índice (IPCA/IGPM) nas informações do contrato ${c["Numero"]}. Diga qual índice usar (ex.: "reajusta pelo IGPM").`);
4049
+ }
4050
+ const idx = await indiceReajusteAcumulado(indice, o.meses ?? 12);
4051
+ const valorAtual = Number(c["Valor"] ?? 0);
4052
+ const novoValor = arred2(valorAtual * idx.fator);
4053
+ return {
4054
+ contrato: Number(c["Numero"]),
4055
+ pessoa: c["PessoaNome"] ? String(c["PessoaNome"]).trim() : undefined,
4056
+ indice,
4057
+ periodo: `${idx.mesInicial} a ${idx.mesFinal}`,
4058
+ indice_referencia: idx.mesFinalLabel,
4059
+ variacao_percent: arred2(idx.percentual),
4060
+ valor_atual: valorAtual,
4061
+ valor_novo: novoValor,
4062
+ diferenca: arred2(novoValor - valorAtual),
4063
+ observacao: "Cálculo apenas — nada foi gravado. Use reajustar_contrato para aplicar.",
4064
+ };
4065
+ }
4066
+ // Campos SÓ de resposta (não devem voltar no PUT do contrato).
4067
+ const CONTRATO_CAMPOS_RESPOSTA = ["ContaNome", "TipoContratoNome", "PessoaNome", "ReajusteStatus", "PagamentoStatus", "PossuiAnexo"];
4068
+ /** Aplica o reajuste no contrato (PUT), acrescenta a nota no texto e, por padrão,
4069
+ * gera as parcelas (GerarFaturamento). Retorna o antes/depois. */
4070
+ export async function reajustarContrato(o) {
4071
+ const c = await resolverContratoCompleto(o);
4072
+ const indice = normalizarIndice(o.indice) ?? detectarIndiceReajuste(c["InformacaoAdicional"]);
4073
+ if (!indice)
4074
+ throw new Error(`Não identifiquei o índice (IPCA/IGPM) nas informações do contrato ${c["Numero"]}. Diga qual índice usar (ex.: "reajusta pelo IGPM").`);
4075
+ const idx = await indiceReajusteAcumulado(indice, o.meses ?? 12);
4076
+ const valorAtual = Number(c["Valor"] ?? 0);
4077
+ const novoValor = arred2(valorAtual * idx.fator);
4078
+ // Eco do próprio objeto do GET (remove campos de resposta), mutando só valor/serviços/nota.
4079
+ const body = { ...c };
4080
+ for (const k of CONTRATO_CAMPOS_RESPOSTA)
4081
+ delete body[k];
4082
+ body["Valor"] = novoValor;
4083
+ if (Array.isArray(body["ContratoServico"])) {
4084
+ body["ContratoServico"] = body["ContratoServico"].map(s => ({
4085
+ ...s,
4086
+ Valor: s["Valor"] != null ? arred2(Number(s["Valor"]) * idx.fator) : s["Valor"],
4087
+ ValorTotal: s["ValorTotal"] != null ? arred2(Number(s["ValorTotal"]) * idx.fator) : s["ValorTotal"],
4088
+ }));
4089
+ }
4090
+ const nota = `Contrato reajustado dia ${dataBR(new Date())}, com Variação ${indice} ${pctBR(idx.percentual)}% (Indice de ${idx.mesFinalLabel}).${o.responsavel ? ` P/${o.responsavel}` : ""}`;
4091
+ const infoAtual = c["InformacaoAdicional"] ? String(c["InformacaoAdicional"]).replace(/\s+$/, "") : "";
4092
+ body["InformacaoAdicional"] = infoAtual ? `${infoAtual}\n\n${nota}` : nota;
4093
+ await apiPut(`/api/Contrato/${encodeURIComponent(String(c["Id"]))}`, body);
4094
+ // Gera as parcelas no valor novo (padrão true).
4095
+ let parcelasGeradas = false;
4096
+ if (o.gerarParcelas !== false) {
4097
+ try {
4098
+ await apiGet(`/api/Contrato/GerarFaturamento/${encodeURIComponent(String(c["Id"]))}`);
4099
+ parcelasGeradas = true;
4100
+ }
4101
+ catch { /* faturamento pode ser feito depois com gerar_parcelas_contrato */ }
4102
+ }
4103
+ return {
4104
+ sucesso: true,
4105
+ contrato: Number(c["Numero"]),
4106
+ pessoa: c["PessoaNome"] ? String(c["PessoaNome"]).trim() : undefined,
4107
+ indice, indice_referencia: idx.mesFinalLabel, variacao_percent: arred2(idx.percentual),
4108
+ valor_anterior: valorAtual, valor_novo: novoValor,
4109
+ parcelas_geradas: parcelasGeradas,
4110
+ nota_adicionada: nota,
4111
+ };
4112
+ }
4113
+ /** Gera (fatura) as parcelas do contrato — GerarFaturamento. */
4114
+ export async function gerarParcelasContrato(o) {
4115
+ const c = await resolverContratoCompleto(o);
4116
+ await apiGet(`/api/Contrato/GerarFaturamento/${encodeURIComponent(String(c["Id"]))}`);
4117
+ return { sucesso: true, contrato: Number(c["Numero"]), mensagem: "Faturamento gerado — as parcelas 'A RECEBER' foram criadas/atualizadas." };
4118
+ }
3874
4119
  /** Monta a ficha completa de um contato a partir do registro slim (precisa do Id). */
3875
4120
  async function detalharContato(p) {
3876
4121
  const id = String(p["Id"] ?? "");
@@ -27,7 +27,7 @@ import * as client from "./client.js";
27
27
  import { createCtx, runInCtx, enterDefaultCtx } from "./context.js";
28
28
  import { toolDefinitions, handleTool, ok, SOFIA_INSTRUCTIONS } from "./tools.js";
29
29
  import { TenantStoreEnv } from "./tenant-store.js";
30
- const VERSION = "0.9.76";
30
+ const VERSION = "0.9.78";
31
31
  // Contexto default (fallback do ALS). No HTTP cada request roda em runInCtx().
32
32
  enterDefaultCtx();
33
33
  // Multi-tenant: NUNCA persistir sessão no disco compartilhado do container.
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ import { openLoginUI } from "./login-server.js";
13
13
  import { toolDefinitions, handleTool, ok, SOFIA_INSTRUCTIONS } from "./tools.js";
14
14
  // Modo stdio: ativa um contexto único global no startup. Multi-tenancy só no modo HTTP.
15
15
  enterDefaultCtx();
16
- const VERSION = "0.9.76";
16
+ const VERSION = "0.9.78";
17
17
  const server = new Server({ name: "SofIA", version: VERSION }, {
18
18
  capabilities: { tools: {} },
19
19
  instructions: SOFIA_INSTRUCTIONS,