senal88 commited on
Commit
cfb5074
·
verified ·
1 Parent(s): c3f7668

chore: deploy web_comercial from monorepo

Browse files
Files changed (43) hide show
  1. .dockerignore +3 -0
  2. Dockerfile +32 -0
  3. README.md +68 -6
  4. apps/web_comercial/.gitignore +41 -0
  5. apps/web_comercial/README.md +16 -0
  6. apps/web_comercial/eslint.config.mjs +18 -0
  7. apps/web_comercial/next-env.d.ts +6 -0
  8. apps/web_comercial/next.config.ts +12 -0
  9. apps/web_comercial/package-lock.json +0 -0
  10. apps/web_comercial/package.json +27 -0
  11. apps/web_comercial/postcss.config.mjs +7 -0
  12. apps/web_comercial/public/file.svg +1 -0
  13. apps/web_comercial/public/globe.svg +1 -0
  14. apps/web_comercial/public/next.svg +1 -0
  15. apps/web_comercial/public/vercel.svg +1 -0
  16. apps/web_comercial/public/window.svg +1 -0
  17. apps/web_comercial/src/app/comparador/page.tsx +291 -0
  18. apps/web_comercial/src/app/configuracoes/page.tsx +125 -0
  19. apps/web_comercial/src/app/favicon.ico +0 -0
  20. apps/web_comercial/src/app/globals.css +259 -0
  21. apps/web_comercial/src/app/layout.tsx +30 -0
  22. apps/web_comercial/src/app/mercado-secundario/page.tsx +534 -0
  23. apps/web_comercial/src/app/page.tsx +57 -0
  24. apps/web_comercial/src/app/premissas/page.tsx +88 -0
  25. apps/web_comercial/src/app/proposta/page.tsx +235 -0
  26. apps/web_comercial/src/components/layout/AppLayout.tsx +19 -0
  27. apps/web_comercial/src/components/layout/Sidebar.tsx +72 -0
  28. apps/web_comercial/src/components/layout/Topbar.tsx +68 -0
  29. apps/web_comercial/src/components/layout/layout.css +350 -0
  30. apps/web_comercial/src/components/simulator/SimulatorForm.tsx +354 -0
  31. apps/web_comercial/src/components/ui/Card.tsx +53 -0
  32. apps/web_comercial/src/components/ui/FormControls.tsx +76 -0
  33. apps/web_comercial/src/components/ui/Table.tsx +59 -0
  34. apps/web_comercial/src/components/ui/Tabs.tsx +29 -0
  35. apps/web_comercial/src/components/ui/Tooltip.tsx +15 -0
  36. apps/web_comercial/src/components/ui/ui.css +318 -0
  37. apps/web_comercial/tsconfig.json +42 -0
  38. packages/simulation-engine/package.json +14 -0
  39. packages/simulation-engine/src/generated/rodobensFieldMatrix.v1.json +838 -0
  40. packages/simulation-engine/src/index.d.ts +196 -0
  41. packages/simulation-engine/src/index.js +823 -0
  42. packages/simulation-engine/src/scpDefaults.js +658 -0
  43. packages/simulation-engine/tests/simulation-engine.test.js +274 -0
.dockerignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ **/.next
2
+ **/node_modules
3
+ **/.DS_Store
Dockerfile ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:22-bookworm-slim AS builder
2
+ WORKDIR /workspace
3
+
4
+ COPY apps/web_comercial ./apps/web_comercial
5
+ COPY packages/simulation-engine ./packages/simulation-engine
6
+
7
+ RUN npm ci --prefix apps/web_comercial
8
+ RUN npm --prefix apps/web_comercial run build
9
+
10
+ FROM node:22-bookworm-slim AS runner
11
+ WORKDIR /workspace
12
+ ENV NODE_ENV=production
13
+ ENV PORT=7860
14
+ ENV HOSTNAME=0.0.0.0
15
+
16
+ # Install Dev Mode requirements
17
+ RUN apt-get update && \
18
+ apt-get install -y \
19
+ bash \
20
+ git git-lfs \
21
+ wget curl procps \
22
+ htop vim nano openssh-client && \
23
+ rm -rf /var/lib/apt/lists/*
24
+
25
+ COPY --from=builder /workspace/apps/web_comercial ./apps/web_comercial
26
+ COPY --from=builder /workspace/packages/simulation-engine ./packages/simulation-engine
27
+
28
+ RUN chown -R 1000:1000 /workspace
29
+ USER 1000
30
+
31
+ EXPOSE 7860
32
+ CMD ["sh", "-c", "cd apps/web_comercial && npm run start -- -H 0.0.0.0 -p 7860"]
README.md CHANGED
@@ -1,10 +1,72 @@
1
  ---
2
- title: Simulador Consorcio Dev
3
- emoji: 📊
4
- colorFrom: pink
5
- colorTo: pink
6
  sdk: docker
7
- pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Plataforma MAIA - Simulador Consórcio
3
+ emoji: 📈
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: docker
7
+ pinned: true
8
+ hardware: zero-a10g
9
  ---
10
 
11
+ ## MAIA v2 - Simulador de Consórcios Corporativo
12
+
13
+ Este é o ambiente de implantação otimizado do **Simulador de Consórcio**, parte da arquitetura **MAIA v2**. O contêiner hospeda nativamente o frontend (`apps/web_comercial` base Next.js) e o motor de simulação por trás (`packages/simulation-engine`), provendo visualizações de fluxos de caixa e regras de negócio para análise de crédito de alto desempenho.
14
+
15
+ ### 🏗 Arquitetura do Contêiner
16
+
17
+ - **Interface:** Baseada em Next.js (Node 22), rodando em modo stand-alone (`apps/web_comercial`).
18
+ - **Motor (Backend):** Pacotes internos de agentes (`packages/agents` e `packages/simulation-engine`) que interagem dinamicamente para aplicar regras complexas de extração e validação do mercado secundário.
19
+ - **Portabiblidade:** Agnóstico à plataforma. Rodamos a mesma imagem Docker em desenvolvimento local e implantação cloud.
20
+
21
+ ### 🚀 Capacidades Premium (Hugging Face PRO)
22
+
23
+ Este Space foi arquitetado sob governança estrita para uso máximo de **Assinatura Hugging Face PRO**:
24
+
25
+ 1. **ZeroGPU e Agnosticismo de Hardware:**
26
+ O sistema solicitará _hardware_ de GPU sob-demanda (`zero-a10g`) durante inferências robustas dos Agentes IA, entregando extrema performance sem consumo residual.
27
+ O utilitário interno Python (`DeviceManager`) é responsável pela segregação: ele ativa CUDA neste ambiente, mas rebaixa graciosamente para _MPS_ (Apple Silicon, M1/M2/M3) ou CPU (Ubuntu VPS/Coolify) em instâncias locais. **Nunca** altere bibliotecas para requerer CUDA como exclusividade.
28
+
29
+ 2. **Depuração Viva (Dev Mode):**
30
+ Como assinante PRO, você pode realizar _hot-fixes_ e testes de inferência na nuvem ativando o **Dev Mode** em `Settings > Dev Mode` do Space. Pode se conectar através de terminal SSH ou usar o **VS Code Web** injetado nativamente pela Hugging Face. As edições realizadas nesse ambiente **SÃO TEMPORÁRIAS E DEVEM SER COMITADAS**.
31
+ O nosso fluxo padrão determina:
32
+ - **`develop` -> Space DEV:** Use o Dev Mode para testes paralelos sem afetar instâncias públicas. Altere o código pelo VS Code do Space e faça `git add`, `git commit` e `git push origin develop`.
33
+ - **`main` -> Space PROD:** Implantações em produção ocorrem automaticamente através de CI/CD limpo sem Dev Mode ativado.
34
+
35
+ 3. **Governança de Dados (Data Studio):**
36
+ Artefatos massivos e pesados (extrações `.xlsm`, logs e tabelas relacionais do simulador) nunca são commitados no repositório de código fonte do GitHub. A análise desses eventos confidenciais corporativos utiliza o portal visual **Data Studio** do ecossistema de dados privados da Hugging Face associado a esta organização.
37
+
38
+ ### ⚙ Variáveis de Ambiente Necessárias (Secrets)
39
+
40
+ Para que os motores e agentes operem de portas abertas com provedores roteados, defina no menu `Settings > Variables and Secrets`:
41
+
42
+ - `HF_TOKEN`: O token da sua conta PRO. Necessário para Inference Providers e APIs avançadas.
43
+ - Qualquer chave adicional de LLMs (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`) deve ser adicionada neste mesmo painel da nuvem, de acordo com o padrão encontrado no `.env` do monorepo original.
44
+
45
+ ### 🔍 Acesso aos Logs (via API)
46
+
47
+ Para depuração avançada sem depender do painel web, a Hugging Face fornece endpoints SSE (_Server-Sent Events_) para observabilidade do Space.
48
+ Você pode usar o script local que configuramos:
49
+
50
+ ```bash
51
+ # Exportar o token antes
52
+ export HF_TOKEN="seu_token_aqui"
53
+
54
+ # Obter logs de execução em tempo real
55
+ ./scripts/deploy/hf_logs.sh sua-org/nome-do-space run
56
+
57
+ # Obter logs de build em tempo real
58
+ ./scripts/deploy/hf_logs.sh sua-org/nome-do-space build
59
+ ```
60
+
61
+ _Ou através de chamadas brutas (cURL)_:
62
+
63
+ ```bash
64
+ curl -N -H "Authorization: Bearer $HF_TOKEN" "https://huggingface.co/api/spaces/sua-org/nome-do-space/logs/run"
65
+ ```
66
+
67
+ ### 🤖 CI/CD do GitHub
68
+
69
+ _Para automação, este repositório é gerenciado upstream pelo GitHub, através do workflow `.github/workflows/simulador-consorcio-deploy-hf.yml`. Ele requer que os seguintes **Repository Secrets** do Github sejam provisionados:_
70
+
71
+ - `HF_SPACE_DEV`: Caminho do Space de desenvolvimento (Ex: `org/simulador-dev`). Disparado em push na `develop`.
72
+ - `HF_SPACE_PROD`: Caminho do Space de produção (Ex: `org/simulador-prod`). Disparado em push na `main`.
apps/web_comercial/.gitignore ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ # production
21
+ /build
22
+
23
+ # misc
24
+ .DS_Store
25
+ *.pem
26
+
27
+ # debug
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ .pnpm-debug.log*
32
+
33
+ # env files (can opt-in for committing if needed)
34
+ .env*
35
+
36
+ # vercel
37
+ .vercel
38
+
39
+ # typescript
40
+ *.tsbuildinfo
41
+ next-env.d.ts
apps/web_comercial/README.md ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Web App - Simulador Consorcio
2
+
3
+ Aplicacao Next.js que consome o motor deterministico interno `@simulador-consorcio/simulation-engine`.
4
+
5
+ ## Desenvolvimento
6
+
7
+ ```bash
8
+ npm run dev
9
+ ```
10
+
11
+ ## Qualidade
12
+
13
+ ```bash
14
+ npm run lint
15
+ npm run build
16
+ ```
apps/web_comercial/eslint.config.mjs ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig, globalIgnores } from "eslint/config";
2
+ import nextVitals from "eslint-config-next/core-web-vitals";
3
+ import nextTs from "eslint-config-next/typescript";
4
+
5
+ const eslintConfig = defineConfig([
6
+ ...nextVitals,
7
+ ...nextTs,
8
+ // Override default ignores of eslint-config-next.
9
+ globalIgnores([
10
+ // Default ignores of eslint-config-next:
11
+ ".next/**",
12
+ "out/**",
13
+ "build/**",
14
+ "next-env.d.ts",
15
+ ]),
16
+ ]);
17
+
18
+ export default eslintConfig;
apps/web_comercial/next-env.d.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+ import "./.next/types/routes.d.ts";
4
+
5
+ // NOTE: This file should not be edited
6
+ // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
apps/web_comercial/next.config.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { NextConfig } from "next";
2
+ import path from "path";
3
+
4
+ const nextConfig: NextConfig = {
5
+ transpilePackages: ["@simulador-consorcio/simulation-engine"],
6
+ outputFileTracingRoot: path.join(__dirname, "../.."),
7
+ experimental: {
8
+ externalDir: true,
9
+ },
10
+ };
11
+
12
+ export default nextConfig;
apps/web_comercial/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
apps/web_comercial/package.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "consorcio-simulator-app",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build --webpack",
8
+ "start": "next start",
9
+ "lint": "eslint"
10
+ },
11
+ "dependencies": {
12
+ "next": "16.1.6",
13
+ "react": "19.2.3",
14
+ "react-dom": "19.2.3",
15
+ "@simulador-consorcio/simulation-engine": "file:../../packages/simulation-engine"
16
+ },
17
+ "devDependencies": {
18
+ "@tailwindcss/postcss": "^4",
19
+ "@types/node": "^20",
20
+ "@types/react": "^19",
21
+ "@types/react-dom": "^19",
22
+ "eslint": "^9",
23
+ "eslint-config-next": "16.1.6",
24
+ "tailwindcss": "^4",
25
+ "typescript": "^5"
26
+ }
27
+ }
apps/web_comercial/postcss.config.mjs ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
apps/web_comercial/public/file.svg ADDED
apps/web_comercial/public/globe.svg ADDED
apps/web_comercial/public/next.svg ADDED
apps/web_comercial/public/vercel.svg ADDED
apps/web_comercial/public/window.svg ADDED
apps/web_comercial/src/app/comparador/page.tsx ADDED
@@ -0,0 +1,291 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { Card, DataCard } from "@/components/ui/Card";
4
+ import { Input, Select } from "@/components/ui/FormControls";
5
+ import { calcularSimulacao } from "@simulador-consorcio/simulation-engine";
6
+ import { useMemo, useState } from "react";
7
+
8
+ function formatCurrency(value: number) {
9
+ return new Intl.NumberFormat("pt-BR", {
10
+ style: "currency",
11
+ currency: "BRL",
12
+ }).format(value);
13
+ }
14
+
15
+ export default function ComparadorPage() {
16
+ const [creditoA, setCreditoA] = useState(500000);
17
+ const [prazoA, setPrazoA] = useState(180);
18
+ const [temLanceA, setTemLanceA] = useState(true);
19
+ const [lancePctA, setLancePctA] = useState(30);
20
+
21
+ const [creditoB, setCreditoB] = useState(500000);
22
+ const [prazoB, setPrazoB] = useState(216);
23
+ const [temLanceB, setTemLanceB] = useState(true);
24
+ const [lancePctB, setLancePctB] = useState(20);
25
+
26
+ const cenarioA = useMemo(() => {
27
+ return calcularSimulacao({
28
+ credito_desejado: creditoA,
29
+ parametros: {
30
+ id_plano: "PADRAO",
31
+ nome_plano: "Plano Padrão A",
32
+ prazo_meses: prazoA,
33
+ taxa_administracao_total: 0.15,
34
+ fundo_reserva_mensal: 0.0005,
35
+ seguro_prestamista: 0.035,
36
+ },
37
+ tem_lance: temLanceA,
38
+ lance_percentual: temLanceA ? lancePctA / 100 : undefined,
39
+ });
40
+ }, [creditoA, prazoA, temLanceA, lancePctA]);
41
+
42
+ const cenarioB = useMemo(() => {
43
+ return calcularSimulacao({
44
+ credito_desejado: creditoB,
45
+ parametros: {
46
+ id_plano: "PADRAO",
47
+ nome_plano: "Plano Padrão B",
48
+ prazo_meses: prazoB,
49
+ taxa_administracao_total: 0.15,
50
+ fundo_reserva_mensal: 0.0005,
51
+ seguro_prestamista: 0.035,
52
+ },
53
+ tem_lance: temLanceB,
54
+ lance_percentual: temLanceB ? lancePctB / 100 : undefined,
55
+ });
56
+ }, [creditoB, prazoB, temLanceB, lancePctB]);
57
+
58
+ return (
59
+ <div className="module-container">
60
+ <div className="module-header">
61
+ <h1 className="module-title">Comparador de Cenários</h1>
62
+ <p className="module-subtitle">
63
+ Análise lado a lado com foco em parcela inicial, prazo, lance e saldo
64
+ operacional da proposta.
65
+ </p>
66
+ </div>
67
+
68
+ <div className="page-kpi-strip">
69
+ <DataCard label="Eixo de decisão" value="Fluxo inicial" subvalue="Peso maior na parcela e no lance" />
70
+ <DataCard label="Base comparada" value="Plano A vs B" subvalue="Prazo, crédito e contemplação" highlight />
71
+ <DataCard label="Leitura" value="Executiva" subvalue="Útil para reunião comercial" />
72
+ </div>
73
+
74
+ <div className="executive-banner">
75
+ <div>
76
+ <strong>Comparação orientada a decisão.</strong> O objetivo aqui é
77
+ evidenciar qual cenário preserva melhor o caixa sem degradar o
78
+ crédito disponível.
79
+ </div>
80
+ <div className="executive-banner__meta">
81
+ <span className="metric-chip">Parcela inicial</span>
82
+ <span className="metric-chip">Prazo total</span>
83
+ <span className="metric-chip">Lance embutido</span>
84
+ </div>
85
+ </div>
86
+
87
+ <div className="module-content" style={{ display: "flex", gap: "24px" }}>
88
+ {/* Cenário A */}
89
+ <div
90
+ style={{
91
+ flex: 1,
92
+ display: "flex",
93
+ flexDirection: "column",
94
+ gap: "16px",
95
+ }}
96
+ >
97
+ <div
98
+ style={{
99
+ padding: "12px 16px",
100
+ backgroundColor: "var(--color-brand-dark)",
101
+ color: "white",
102
+ borderRadius: "4px",
103
+ fontWeight: 600,
104
+ textAlign: "center",
105
+ }}
106
+ >
107
+ Cenário A
108
+ </div>
109
+ <Card title="Parâmetros">
110
+ <div
111
+ style={{ display: "flex", flexDirection: "column", gap: "12px" }}
112
+ >
113
+ <Input
114
+ label="Crédito (R$)"
115
+ type="number"
116
+ value={creditoA}
117
+ onChange={(e) => setCreditoA(Number(e.target.value))}
118
+ />
119
+ <Select
120
+ label="Prazo (meses)"
121
+ value={prazoA}
122
+ onChange={(e) => setPrazoA(Number(e.target.value))}
123
+ options={[
124
+ { label: "120", value: 120 },
125
+ { label: "150", value: 150 },
126
+ { label: "180", value: 180 },
127
+ { label: "216", value: 216 },
128
+ ]}
129
+ />
130
+ <div className="ui-form-group">
131
+ <label
132
+ className="ui-form-label"
133
+ style={{ display: "flex", alignItems: "center", gap: "8px" }}
134
+ >
135
+ <input
136
+ type="checkbox"
137
+ checked={temLanceA}
138
+ onChange={(e) => setTemLanceA(e.target.checked)}
139
+ />{" "}
140
+ Com Lance?
141
+ </label>
142
+ </div>
143
+ {temLanceA && (
144
+ <Input
145
+ label="Lance (%)"
146
+ type="number"
147
+ value={lancePctA}
148
+ onChange={(e) => setLancePctA(Number(e.target.value))}
149
+ />
150
+ )}
151
+ </div>
152
+ </Card>
153
+ <Card title="Sumário" highlight>
154
+ <div
155
+ style={{
156
+ padding: "16px",
157
+ display: "flex",
158
+ flexDirection: "column",
159
+ gap: "12px",
160
+ }}
161
+ >
162
+ <DataCard
163
+ label="Crédito"
164
+ value={formatCurrency(cenarioA.credito)}
165
+ />
166
+ <DataCard
167
+ label="Parcela Inicial Integral"
168
+ value={formatCurrency(cenarioA.parcela_integral.total)}
169
+ highlight
170
+ />
171
+ <DataCard label="Prazo Total" value={`${cenarioA.prazo} meses`} />
172
+ {temLanceA && cenarioA.valor_lance && (
173
+ <DataCard
174
+ label="Lance Embutido Estimado"
175
+ value={`${formatCurrency(cenarioA.valor_lance)} (${lancePctA}%)`}
176
+ />
177
+ )}
178
+ </div>
179
+ </Card>
180
+ </div>
181
+
182
+ {/* VS Divider */}
183
+ <div
184
+ style={{
185
+ display: "flex",
186
+ alignItems: "center",
187
+ justifyContent: "center",
188
+ fontWeight: "bold",
189
+ color: "var(--text-muted)",
190
+ }}
191
+ >
192
+ VS
193
+ </div>
194
+
195
+ {/* Cenário B */}
196
+ <div
197
+ style={{
198
+ flex: 1,
199
+ display: "flex",
200
+ flexDirection: "column",
201
+ gap: "16px",
202
+ }}
203
+ >
204
+ <div
205
+ style={{
206
+ padding: "12px 16px",
207
+ backgroundColor: "var(--color-brand-accent)",
208
+ color: "white",
209
+ borderRadius: "4px",
210
+ fontWeight: 600,
211
+ textAlign: "center",
212
+ }}
213
+ >
214
+ Cenário B
215
+ </div>
216
+ <Card title="Parâmetros">
217
+ <div
218
+ style={{ display: "flex", flexDirection: "column", gap: "12px" }}
219
+ >
220
+ <Input
221
+ label="Crédito (R$)"
222
+ type="number"
223
+ value={creditoB}
224
+ onChange={(e) => setCreditoB(Number(e.target.value))}
225
+ />
226
+ <Select
227
+ label="Prazo (meses)"
228
+ value={prazoB}
229
+ onChange={(e) => setPrazoB(Number(e.target.value))}
230
+ options={[
231
+ { label: "120", value: 120 },
232
+ { label: "150", value: 150 },
233
+ { label: "180", value: 180 },
234
+ { label: "216", value: 216 },
235
+ ]}
236
+ />
237
+ <div className="ui-form-group">
238
+ <label
239
+ className="ui-form-label"
240
+ style={{ display: "flex", alignItems: "center", gap: "8px" }}
241
+ >
242
+ <input
243
+ type="checkbox"
244
+ checked={temLanceB}
245
+ onChange={(e) => setTemLanceB(e.target.checked)}
246
+ />{" "}
247
+ Com Lance?
248
+ </label>
249
+ </div>
250
+ {temLanceB && (
251
+ <Input
252
+ label="Lance (%)"
253
+ type="number"
254
+ value={lancePctB}
255
+ onChange={(e) => setLancePctB(Number(e.target.value))}
256
+ />
257
+ )}
258
+ </div>
259
+ </Card>
260
+ <Card title="Sumário" highlight>
261
+ <div
262
+ style={{
263
+ padding: "16px",
264
+ display: "flex",
265
+ flexDirection: "column",
266
+ gap: "12px",
267
+ }}
268
+ >
269
+ <DataCard
270
+ label="Crédito"
271
+ value={formatCurrency(cenarioB.credito)}
272
+ />
273
+ <DataCard
274
+ label="Parcela Inicial Integral"
275
+ value={formatCurrency(cenarioB.parcela_integral.total)}
276
+ highlight
277
+ />
278
+ <DataCard label="Prazo Total" value={`${cenarioB.prazo} meses`} />
279
+ {temLanceB && cenarioB.valor_lance && (
280
+ <DataCard
281
+ label="Lance Embutido Estimado"
282
+ value={`${formatCurrency(cenarioB.valor_lance)} (${lancePctB}%)`}
283
+ />
284
+ )}
285
+ </div>
286
+ </Card>
287
+ </div>
288
+ </div>
289
+ </div>
290
+ );
291
+ }
apps/web_comercial/src/app/configuracoes/page.tsx ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Card, DataCard } from "@/components/ui/Card";
2
+ import { Select } from "@/components/ui/FormControls";
3
+
4
+ export default function ConfiguracoesPage() {
5
+ return (
6
+ <div className="module-container">
7
+ <div className="module-header">
8
+ <h1 className="module-title">Configurações de Visualização</h1>
9
+ <p className="module-subtitle">
10
+ Ajuste densidade, modo de leitura e profundidade de memória de cálculo.
11
+ </p>
12
+ </div>
13
+
14
+ <div className="page-kpi-strip">
15
+ <DataCard label="Perfil padrão" value="Operacional" subvalue="Leitura detalhada de backoffice" />
16
+ <DataCard label="Saída cliente" value="Executivo" subvalue="Condensa ruído e prioriza proposta" highlight />
17
+ <DataCard label="Granularidade" value="Configuração fina" subvalue="Composição, cronograma e visão consolidada" />
18
+ </div>
19
+
20
+ <div
21
+ className="module-content"
22
+ style={{ display: "flex", flexDirection: "column", gap: "24px" }}
23
+ >
24
+ <Card title="Modo de Operação">
25
+ <div
26
+ style={{
27
+ display: "grid",
28
+ gridTemplateColumns: "1fr 1fr",
29
+ gap: "16px",
30
+ }}
31
+ >
32
+ <Select
33
+ label="Modo"
34
+ options={[
35
+ { label: "Operacional (Detalhado)", value: "operacional" },
36
+ { label: "Executivo (Sumarizado)", value: "executivo" },
37
+ { label: "Cliente (Simplificado)", value: "cliente" },
38
+ ]}
39
+ defaultValue="operacional"
40
+ />
41
+ <Select
42
+ label="Densidade da Interface"
43
+ options={[
44
+ { label: "Compacta (Mais dados)", value: "compacta" },
45
+ { label: "Confortável (Melhor leitura)", value: "confortavel" },
46
+ ]}
47
+ defaultValue="compacta"
48
+ />
49
+ </div>
50
+ </Card>
51
+
52
+ <Card title="Preferências do Simulador">
53
+ <div
54
+ style={{ display: "grid", gridTemplateColumns: "1fr", gap: "12px" }}
55
+ >
56
+ <label
57
+ style={{
58
+ display: "flex",
59
+ alignItems: "center",
60
+ gap: "8px",
61
+ fontSize: "0.9rem",
62
+ }}
63
+ >
64
+ <input type="checkbox" defaultChecked /> Mostrar composição da
65
+ parcela
66
+ </label>
67
+ <label
68
+ style={{
69
+ display: "flex",
70
+ alignItems: "center",
71
+ gap: "8px",
72
+ fontSize: "0.9rem",
73
+ }}
74
+ >
75
+ <input type="checkbox" defaultChecked /> Mostrar memória de
76
+ cálculo expandida
77
+ </label>
78
+ <label
79
+ style={{
80
+ display: "flex",
81
+ alignItems: "center",
82
+ gap: "8px",
83
+ fontSize: "0.9rem",
84
+ }}
85
+ >
86
+ <input type="checkbox" /> Mostrar cronograma mensal completo
87
+ </label>
88
+ <label
89
+ style={{
90
+ display: "flex",
91
+ alignItems: "center",
92
+ gap: "8px",
93
+ fontSize: "0.9rem",
94
+ }}
95
+ >
96
+ <input type="checkbox" defaultChecked /> Destacar números
97
+ absolutos vs percentuais
98
+ </label>
99
+ <label
100
+ style={{
101
+ display: "flex",
102
+ alignItems: "center",
103
+ gap: "8px",
104
+ fontSize: "0.9rem",
105
+ }}
106
+ >
107
+ <input type="checkbox" /> Visão consolidada por múltiplas cotas
108
+ </label>
109
+ </div>
110
+ </Card>
111
+
112
+ <div
113
+ style={{
114
+ display: "flex",
115
+ justifyContent: "flex-end",
116
+ gap: "12px",
117
+ marginTop: "16px",
118
+ }}
119
+ >
120
+ <button className="btn btn-primary">Aplicar Configurações</button>
121
+ </div>
122
+ </div>
123
+ </div>
124
+ );
125
+ }
apps/web_comercial/src/app/favicon.ico ADDED
apps/web_comercial/src/app/globals.css ADDED
@@ -0,0 +1,259 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+
3
+ :root {
4
+ --background: #f3f0eb;
5
+ --foreground: #1c2733;
6
+ --bg-main: #ece7e0;
7
+ --bg-panel: #ffffff;
8
+ --bg-panel-soft: #f8f5f1;
9
+ --bg-panel-accent: #edf4fb;
10
+ --bg-panel-dark: #0f2743;
11
+ --bg-sidebar: #09213d;
12
+ --border-color: #d7d2c8;
13
+ --border-strong: #c5bcae;
14
+ --text-main: #1c2733;
15
+ --text-muted: #667285;
16
+ --text-soft: #8f9aac;
17
+ --color-brand-dark: #0d2d52;
18
+ --color-brand-accent: #ec5b13;
19
+ --color-brand-accent-strong: #cb4610;
20
+ --color-brand-light: #dcebf7;
21
+ --status-positive: #1c7c54;
22
+ --status-warning: #9a6700;
23
+ --status-negative: #c2410c;
24
+ --status-info: #2563eb;
25
+ --shadow-panel: 0 10px 24px rgba(11, 27, 49, 0.06);
26
+ --shadow-soft: 0 2px 6px rgba(15, 39, 67, 0.08);
27
+ }
28
+
29
+ @theme inline {
30
+ --color-background: var(--background);
31
+ --color-foreground: var(--foreground);
32
+ --font-sans: "Public Sans", "IBM Plex Sans", "Aptos", "Segoe UI", sans-serif;
33
+ --font-mono: "SFMono-Regular", "Menlo", "Monaco", "Consolas", monospace;
34
+ }
35
+
36
+ * {
37
+ box-sizing: border-box;
38
+ }
39
+
40
+ html,
41
+ body {
42
+ min-height: 100%;
43
+ }
44
+
45
+ body {
46
+ margin: 0;
47
+ background:
48
+ linear-gradient(180deg, rgba(255, 255, 255, 0.55), rgba(255, 255, 255, 0))
49
+ 0 0 / 100% 180px no-repeat,
50
+ var(--background);
51
+ color: var(--text-main);
52
+ font-family: var(--font-sans);
53
+ }
54
+
55
+ a {
56
+ color: inherit;
57
+ }
58
+
59
+ button,
60
+ input,
61
+ select,
62
+ textarea {
63
+ font: inherit;
64
+ }
65
+
66
+ .page-kpi-strip {
67
+ display: grid;
68
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
69
+ gap: 12px;
70
+ margin-bottom: 20px;
71
+ }
72
+
73
+ .executive-banner {
74
+ display: flex;
75
+ justify-content: space-between;
76
+ gap: 16px;
77
+ align-items: center;
78
+ padding: 14px 18px;
79
+ border: 1px solid var(--border-color);
80
+ border-left: 4px solid var(--color-brand-accent);
81
+ border-radius: 8px;
82
+ background: linear-gradient(90deg, rgba(236, 91, 19, 0.08), rgba(13, 45, 82, 0.04));
83
+ color: var(--text-main);
84
+ }
85
+
86
+ .executive-banner strong {
87
+ color: var(--color-brand-dark);
88
+ }
89
+
90
+ .executive-banner__meta {
91
+ display: flex;
92
+ flex-wrap: wrap;
93
+ gap: 8px;
94
+ }
95
+
96
+ .metric-chip,
97
+ .status-chip {
98
+ display: inline-flex;
99
+ align-items: center;
100
+ gap: 6px;
101
+ min-height: 30px;
102
+ padding: 0 10px;
103
+ border-radius: 999px;
104
+ font-size: 0.73rem;
105
+ font-weight: 700;
106
+ letter-spacing: 0.04em;
107
+ text-transform: uppercase;
108
+ }
109
+
110
+ .metric-chip {
111
+ background: rgba(13, 45, 82, 0.08);
112
+ color: var(--color-brand-dark);
113
+ }
114
+
115
+ .status-chip {
116
+ border: 1px solid transparent;
117
+ }
118
+
119
+ .status-chip.status-live {
120
+ background: rgba(28, 124, 84, 0.1);
121
+ color: var(--status-positive);
122
+ border-color: rgba(28, 124, 84, 0.18);
123
+ }
124
+
125
+ .status-chip.status-warning {
126
+ background: rgba(154, 103, 0, 0.1);
127
+ color: var(--status-warning);
128
+ border-color: rgba(154, 103, 0, 0.2);
129
+ }
130
+
131
+ .status-chip.status-info {
132
+ background: rgba(37, 99, 235, 0.08);
133
+ color: var(--status-info);
134
+ border-color: rgba(37, 99, 235, 0.18);
135
+ }
136
+
137
+ .section-grid {
138
+ display: grid;
139
+ gap: 20px;
140
+ }
141
+
142
+ .section-grid--2 {
143
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
144
+ }
145
+
146
+ .section-grid--3 {
147
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
148
+ }
149
+
150
+ .section-grid--simulator {
151
+ grid-template-columns: minmax(0, 1.75fr) minmax(320px, 0.95fr);
152
+ }
153
+
154
+ .section-stack {
155
+ display: flex;
156
+ flex-direction: column;
157
+ gap: 20px;
158
+ }
159
+
160
+ .field-grid {
161
+ display: grid;
162
+ gap: 16px;
163
+ }
164
+
165
+ .field-grid--2 {
166
+ grid-template-columns: repeat(2, minmax(0, 1fr));
167
+ }
168
+
169
+ .field-grid--3 {
170
+ grid-template-columns: repeat(3, minmax(0, 1fr));
171
+ }
172
+
173
+ .field-grid--4 {
174
+ grid-template-columns: repeat(4, minmax(0, 1fr));
175
+ }
176
+
177
+ .summary-rail {
178
+ display: flex;
179
+ flex-direction: column;
180
+ gap: 20px;
181
+ }
182
+
183
+ .plain-note {
184
+ margin: 0;
185
+ font-size: 0.84rem;
186
+ color: var(--text-muted);
187
+ line-height: 1.55;
188
+ }
189
+
190
+ .inline-toggle {
191
+ display: flex;
192
+ align-items: center;
193
+ gap: 10px;
194
+ font-size: 0.84rem;
195
+ color: var(--text-main);
196
+ }
197
+
198
+ .table-note {
199
+ display: flex;
200
+ justify-content: space-between;
201
+ gap: 12px;
202
+ padding: 12px 16px;
203
+ border-top: 1px solid var(--border-color);
204
+ background: var(--bg-panel-soft);
205
+ color: var(--text-muted);
206
+ font-size: 0.78rem;
207
+ }
208
+
209
+ .sheet-frame {
210
+ background: var(--bg-panel);
211
+ border: 1px solid var(--border-strong);
212
+ border-radius: 10px;
213
+ box-shadow: var(--shadow-panel);
214
+ }
215
+
216
+ .sheet-section-title {
217
+ display: flex;
218
+ align-items: center;
219
+ justify-content: space-between;
220
+ gap: 12px;
221
+ margin-bottom: 14px;
222
+ }
223
+
224
+ .sheet-section-title h2,
225
+ .sheet-section-title h3 {
226
+ margin: 0;
227
+ font-size: 1rem;
228
+ font-weight: 700;
229
+ color: var(--color-brand-dark);
230
+ }
231
+
232
+ .muted-divider {
233
+ height: 1px;
234
+ background: var(--border-color);
235
+ margin: 8px 0 0;
236
+ }
237
+
238
+ @media (max-width: 1180px) {
239
+ .section-grid--simulator {
240
+ grid-template-columns: 1fr;
241
+ }
242
+
243
+ .field-grid--4 {
244
+ grid-template-columns: repeat(2, minmax(0, 1fr));
245
+ }
246
+ }
247
+
248
+ @media (max-width: 768px) {
249
+ .executive-banner {
250
+ flex-direction: column;
251
+ align-items: flex-start;
252
+ }
253
+
254
+ .field-grid--2,
255
+ .field-grid--3,
256
+ .field-grid--4 {
257
+ grid-template-columns: 1fr;
258
+ }
259
+ }
apps/web_comercial/src/app/layout.tsx ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from "next";
2
+ import { Public_Sans } from "next/font/google";
3
+ import { AppLayout } from "@/components/layout/AppLayout";
4
+ import "./globals.css";
5
+ import "@/components/layout/layout.css";
6
+ import "@/components/ui/ui.css";
7
+
8
+ const publicSans = Public_Sans({
9
+ subsets: ["latin"],
10
+ display: "swap",
11
+ });
12
+
13
+ export const metadata: Metadata = {
14
+ title: "Apex Consorcios | Simulador",
15
+ description: "Simulador deterministico de consorcio imobiliario",
16
+ };
17
+
18
+ export default function RootLayout({
19
+ children,
20
+ }: Readonly<{
21
+ children: React.ReactNode;
22
+ }>) {
23
+ return (
24
+ <html lang="pt-BR">
25
+ <body className={`${publicSans.className} antialiased`}>
26
+ <AppLayout>{children}</AppLayout>
27
+ </body>
28
+ </html>
29
+ );
30
+ }
apps/web_comercial/src/app/mercado-secundario/page.tsx ADDED
@@ -0,0 +1,534 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useMemo, useState } from "react";
4
+ import {
5
+ listarPerfisFluxoScpDefault,
6
+ simularModoConversaoScpConsorcio,
7
+ simularSecundarioComPerfilScp,
8
+ type ResultadoSimulacaoConversaoScpConsorcio,
9
+ type ResultadoSimulacaoSecundarioComPerfil,
10
+ } from "@simulador-consorcio/simulation-engine";
11
+ import { Card, DataCard } from "@/components/ui/Card";
12
+ import { Input, Select } from "@/components/ui/FormControls";
13
+ import {
14
+ Table,
15
+ TableBody,
16
+ TableCell,
17
+ TableHead,
18
+ TableHeader,
19
+ TableRow,
20
+ } from "@/components/ui/Table";
21
+
22
+ const catalogoDefault = listarPerfisFluxoScpDefault();
23
+ type ModoSimulacao = "secundario_padrao" | "conversao_scp_imovel_consorcio";
24
+ type ResultadoTela =
25
+ | ResultadoSimulacaoSecundarioComPerfil
26
+ | ResultadoSimulacaoConversaoScpConsorcio;
27
+
28
+ function formatCurrency(value: number) {
29
+ return new Intl.NumberFormat("pt-BR", {
30
+ style: "currency",
31
+ currency: "BRL",
32
+ }).format(value);
33
+ }
34
+
35
+ function formatPercent(value: number | null | undefined) {
36
+ if (value === null || value === undefined) {
37
+ return "--";
38
+ }
39
+ return `${(value * 100).toFixed(2)}%`;
40
+ }
41
+
42
+ export default function MercadoSecundarioPage() {
43
+ const activeClient =
44
+ catalogoDefault.clients.find((client) => client.status === "ativo") ||
45
+ catalogoDefault.clients[0];
46
+
47
+ const [modoSimulacao, setModoSimulacao] =
48
+ useState<ModoSimulacao>("secundario_padrao");
49
+ const [clienteId, setClienteId] = useState(activeClient.client_id);
50
+ const [patternId, setPatternId] = useState(activeClient.default_pattern_id);
51
+ const [novoClienteId, setNovoClienteId] = useState("");
52
+ const [novoClienteNome, setNovoClienteNome] = useState("");
53
+ const [escalaFluxo, setEscalaFluxo] = useState(1);
54
+ const [deslocamentoMeses, setDeslocamentoMeses] = useState(0);
55
+ const [portabilidadeContempladaPct, setPortabilidadeContempladaPct] =
56
+ useState(1);
57
+ const [portabilidadeNaoContempladaPct, setPortabilidadeNaoContempladaPct] =
58
+ useState(0.5);
59
+ const [feePct, setFeePct] = useState(2);
60
+ const [volumeContratado, setVolumeContratado] = useState(300000);
61
+ const [percentualCartasReduzidaPct, setPercentualCartasReduzidaPct] =
62
+ useState(100);
63
+ const [mesesPosContemplacao, setMesesPosContemplacao] = useState(0);
64
+ const [valorImovelBase, setValorImovelBase] = useState(600000);
65
+ const [descontoImovelPct, setDescontoImovelPct] = useState(15);
66
+ const [percentualEntradaImovelPct, setPercentualEntradaImovelPct] =
67
+ useState(25);
68
+ const [agioConsorcioPct, setAgioConsorcioPct] = useState(10);
69
+ const [taxaCustosTransacaoPct, setTaxaCustosTransacaoPct] = useState(2);
70
+ const [resultado, setResultado] = useState<ResultadoTela | null>(null);
71
+ const [erro, setErro] = useState<string | null>(null);
72
+
73
+ const clienteSelecionado = useMemo(
74
+ () => catalogoDefault.clients.find((client) => client.client_id === clienteId),
75
+ [clienteId]
76
+ );
77
+
78
+ const resumoFluxo = useMemo(() => {
79
+ if (!resultado) {
80
+ return null;
81
+ }
82
+ return resultado.fluxo_cliente.pontos.slice(0, 12);
83
+ }, [resultado]);
84
+
85
+ const handleClienteChange = (nextClientId: string) => {
86
+ setClienteId(nextClientId);
87
+ const client = catalogoDefault.clients.find((item) => item.client_id === nextClientId);
88
+ if (client) {
89
+ setPatternId(client.default_pattern_id);
90
+ }
91
+ };
92
+
93
+ const handleSimular = () => {
94
+ try {
95
+ setErro(null);
96
+ const baseInput = {
97
+ client_id: clienteId,
98
+ pattern_id: patternId,
99
+ novo_client_id: novoClienteId || undefined,
100
+ novo_client_name: novoClienteNome || undefined,
101
+ escala_fluxo: escalaFluxo,
102
+ deslocamento_meses: deslocamentoMeses,
103
+ portabilidade_percentual_contemplada: portabilidadeContempladaPct / 100,
104
+ portabilidade_percentual_nao_contemplada:
105
+ portabilidadeNaoContempladaPct / 100,
106
+ fee_percentual: feePct / 100,
107
+ volume_contratado: volumeContratado,
108
+ percentual_cartas_parcela_reduzida: percentualCartasReduzidaPct / 100,
109
+ meses_pos_ultima_contemplacao: mesesPosContemplacao,
110
+ };
111
+ const simulacao =
112
+ modoSimulacao === "conversao_scp_imovel_consorcio"
113
+ ? simularModoConversaoScpConsorcio({
114
+ ...baseInput,
115
+ valor_imovel_base: valorImovelBase,
116
+ desconto_imovel_percentual: descontoImovelPct / 100,
117
+ percentual_entrada_imovel: percentualEntradaImovelPct / 100,
118
+ agio_consorcio_percentual: agioConsorcioPct / 100,
119
+ taxa_custos_transacao_percentual: taxaCustosTransacaoPct / 100,
120
+ })
121
+ : simularSecundarioComPerfilScp(baseInput);
122
+ setResultado(simulacao);
123
+ } catch (simulationError) {
124
+ const message =
125
+ simulationError instanceof Error
126
+ ? simulationError.message
127
+ : "Falha ao simular cenário do mercado secundário.";
128
+ setErro(message);
129
+ setResultado(null);
130
+ }
131
+ };
132
+
133
+ return (
134
+ <div className="module-container">
135
+ <div
136
+ className="module-header"
137
+ style={{
138
+ display: "flex",
139
+ justifyContent: "space-between",
140
+ alignItems: "center",
141
+ }}
142
+ >
143
+ <div>
144
+ <h1 className="module-title">Mercado Secundário</h1>
145
+ <p className="module-subtitle">
146
+ Simulação parametrizada por cliente SCP com base nos padrões de fluxo
147
+ entregues.
148
+ </p>
149
+ </div>
150
+ <div style={{ display: "flex", gap: "12px" }}>
151
+ <button className="btn btn-secondary">Exportar Relatório</button>
152
+ <button className="btn btn-primary" onClick={handleSimular}>
153
+ Simular Cenário
154
+ </button>
155
+ </div>
156
+ </div>
157
+
158
+ <div className="page-kpi-strip">
159
+ <DataCard
160
+ label="Carteira"
161
+ value={clienteSelecionado?.client_name ?? "Cliente SCP"}
162
+ subvalue={`Padrão ${patternId}`}
163
+ />
164
+ <DataCard
165
+ label="Modo"
166
+ value={
167
+ modoSimulacao === "conversao_scp_imovel_consorcio"
168
+ ? "Conversão SCP"
169
+ : "Secundário padrão"
170
+ }
171
+ subvalue="Fluxo operacional por cliente"
172
+ highlight
173
+ />
174
+ <DataCard
175
+ label="Volume contratado"
176
+ value={formatCurrency(volumeContratado)}
177
+ subvalue={`${percentualCartasReduzidaPct}% da carteira reduzida`}
178
+ />
179
+ <DataCard
180
+ label="Fee"
181
+ value={`${feePct.toFixed(2)}%`}
182
+ subvalue={`Deslocamento ${deslocamentoMeses} meses`}
183
+ />
184
+ </div>
185
+
186
+ <div className="executive-banner">
187
+ <div>
188
+ <strong>Mercado secundário com leitura operacional.</strong> A tela
189
+ combina fluxo SCP, portabilidade e conversão para compra de imóvel em
190
+ uma visão única de decisão.
191
+ </div>
192
+ <div className="executive-banner__meta">
193
+ <span className="metric-chip">Fluxo cliente</span>
194
+ <span className="metric-chip">Resultado APEX</span>
195
+ <span className="metric-chip">Retorno anual</span>
196
+ </div>
197
+ </div>
198
+
199
+ <div
200
+ className="module-content"
201
+ style={{ display: "flex", flexDirection: "column", gap: "24px" }}
202
+ >
203
+ <Card title="Cliente e Padrão de Fluxo SCP">
204
+ <div
205
+ style={{
206
+ display: "grid",
207
+ gridTemplateColumns: "repeat(4, minmax(180px, 1fr))",
208
+ gap: "16px",
209
+ }}
210
+ >
211
+ <Select
212
+ label="Modo de Simulação"
213
+ value={modoSimulacao}
214
+ onChange={(event) =>
215
+ setModoSimulacao(event.target.value as ModoSimulacao)
216
+ }
217
+ options={[
218
+ { value: "secundario_padrao", label: "Secundário padrão" },
219
+ {
220
+ value: "conversao_scp_imovel_consorcio",
221
+ label: "Conversão SCP -> Imóvel + Secundário",
222
+ },
223
+ ]}
224
+ />
225
+ <Select
226
+ label="Cliente Default"
227
+ value={clienteId}
228
+ onChange={(event) => handleClienteChange(event.target.value)}
229
+ options={catalogoDefault.clients.map((client) => ({
230
+ value: client.client_id,
231
+ label: `${client.client_name} (${client.status})`,
232
+ }))}
233
+ />
234
+ <Select
235
+ label="Padrão de Fluxo"
236
+ value={patternId}
237
+ onChange={(event) => setPatternId(event.target.value)}
238
+ options={catalogoDefault.patterns.map((pattern) => ({
239
+ value: pattern.id,
240
+ label: `${pattern.nome} (${pattern.pontos} pontos)`,
241
+ }))}
242
+ />
243
+ <Input
244
+ label="Novo Cliente ID (opcional)"
245
+ value={novoClienteId}
246
+ onChange={(event) => setNovoClienteId(event.target.value)}
247
+ placeholder="cliente_scp_001"
248
+ />
249
+ <Input
250
+ label="Novo Cliente Nome (opcional)"
251
+ value={novoClienteNome}
252
+ onChange={(event) => setNovoClienteNome(event.target.value)}
253
+ placeholder="Cliente SCP 001"
254
+ />
255
+ <Input
256
+ label="Escala de Fluxo"
257
+ type="number"
258
+ value={escalaFluxo}
259
+ onChange={(event) => setEscalaFluxo(Number(event.target.value || 0))}
260
+ step="0.1"
261
+ />
262
+ <Input
263
+ label="Deslocamento (meses)"
264
+ type="number"
265
+ value={deslocamentoMeses}
266
+ onChange={(event) =>
267
+ setDeslocamentoMeses(Number(event.target.value || 0))
268
+ }
269
+ step="1"
270
+ />
271
+ <Input
272
+ label="Portabilidade Contemplada"
273
+ type="number"
274
+ value={portabilidadeContempladaPct}
275
+ onChange={(event) =>
276
+ setPortabilidadeContempladaPct(Number(event.target.value || 0))
277
+ }
278
+ suffix="%"
279
+ />
280
+ <Input
281
+ label="Portabilidade Não Contemplada"
282
+ type="number"
283
+ value={portabilidadeNaoContempladaPct}
284
+ onChange={(event) =>
285
+ setPortabilidadeNaoContempladaPct(Number(event.target.value || 0))
286
+ }
287
+ suffix="%"
288
+ />
289
+ <Input
290
+ label="Fee"
291
+ type="number"
292
+ value={feePct}
293
+ onChange={(event) => setFeePct(Number(event.target.value || 0))}
294
+ suffix="%"
295
+ />
296
+ <Input
297
+ label="Volume Contratado"
298
+ type="number"
299
+ value={volumeContratado}
300
+ onChange={(event) =>
301
+ setVolumeContratado(Number(event.target.value || 0))
302
+ }
303
+ prefix="R$"
304
+ />
305
+ <Input
306
+ label="% Cartas com Parcela Reduzida"
307
+ type="number"
308
+ value={percentualCartasReduzidaPct}
309
+ onChange={(event) =>
310
+ setPercentualCartasReduzidaPct(Number(event.target.value || 0))
311
+ }
312
+ suffix="%"
313
+ />
314
+ <Input
315
+ label="Meses Pós Última Contemplação"
316
+ type="number"
317
+ value={mesesPosContemplacao}
318
+ onChange={(event) =>
319
+ setMesesPosContemplacao(Number(event.target.value || 0))
320
+ }
321
+ step="1"
322
+ />
323
+ {modoSimulacao === "conversao_scp_imovel_consorcio" && (
324
+ <>
325
+ <Input
326
+ label="Valor Base do Imóvel"
327
+ type="number"
328
+ value={valorImovelBase}
329
+ onChange={(event) =>
330
+ setValorImovelBase(Number(event.target.value || 0))
331
+ }
332
+ prefix="R$"
333
+ />
334
+ <Input
335
+ label="Desconto Imóvel"
336
+ type="number"
337
+ value={descontoImovelPct}
338
+ onChange={(event) =>
339
+ setDescontoImovelPct(Number(event.target.value || 0))
340
+ }
341
+ suffix="%"
342
+ />
343
+ <Input
344
+ label="Entrada da Compra"
345
+ type="number"
346
+ value={percentualEntradaImovelPct}
347
+ onChange={(event) =>
348
+ setPercentualEntradaImovelPct(Number(event.target.value || 0))
349
+ }
350
+ suffix="%"
351
+ />
352
+ <Input
353
+ label="Ágio no Secundário"
354
+ type="number"
355
+ value={agioConsorcioPct}
356
+ onChange={(event) =>
357
+ setAgioConsorcioPct(Number(event.target.value || 0))
358
+ }
359
+ suffix="%"
360
+ />
361
+ <Input
362
+ label="Custos de Transação"
363
+ type="number"
364
+ value={taxaCustosTransacaoPct}
365
+ onChange={(event) =>
366
+ setTaxaCustosTransacaoPct(Number(event.target.value || 0))
367
+ }
368
+ suffix="%"
369
+ />
370
+ </>
371
+ )}
372
+ </div>
373
+ {clienteSelecionado && (
374
+ <p style={{ marginTop: "12px", color: "var(--text-muted)" }}>
375
+ Cliente selecionado: <strong>{clienteSelecionado.client_name}</strong>.
376
+ Padrão default: <code>{clienteSelecionado.default_pattern_id}</code>.
377
+ </p>
378
+ )}
379
+ {modoSimulacao === "conversao_scp_imovel_consorcio" && (
380
+ <p style={{ marginTop: "6px", color: "var(--text-muted)" }}>
381
+ Modo conversão é um cenário fechado parametrizado para SCP -&gt; compra
382
+ de imóvel com desconto + operação no secundário.
383
+ </p>
384
+ )}
385
+ </Card>
386
+
387
+ {erro && (
388
+ <Card title="Erro de Simulação">
389
+ <p style={{ color: "var(--status-negative)" }}>{erro}</p>
390
+ </Card>
391
+ )}
392
+
393
+ <Card title="Resultado do Cenário" noPadding>
394
+ {resultado ? (
395
+ <div style={{ padding: "20px", display: "flex", flexDirection: "column", gap: "20px" }}>
396
+ <div
397
+ style={{
398
+ display: "grid",
399
+ gridTemplateColumns: "repeat(4, minmax(170px, 1fr))",
400
+ gap: "12px",
401
+ }}
402
+ >
403
+ <DataCard
404
+ label="Payback"
405
+ value={
406
+ resultado.payback_meses === null
407
+ ? "Sem retorno"
408
+ : `${resultado.payback_meses} meses`
409
+ }
410
+ />
411
+ <DataCard
412
+ label="TIR Mensal"
413
+ value={formatPercent(resultado.tir_mensal)}
414
+ />
415
+ <DataCard
416
+ label="TIR Anual"
417
+ value={formatPercent(resultado.tir_anual)}
418
+ />
419
+ <DataCard
420
+ label="Caixa Máximo Negativo"
421
+ value={formatCurrency(resultado.caixa_maximo_negativo)}
422
+ />
423
+ <DataCard
424
+ label="Custo Portabilidade"
425
+ value={formatCurrency(resultado.custo_portabilidade)}
426
+ />
427
+ <DataCard
428
+ label="Custo Fee 70%"
429
+ value={formatCurrency(resultado.custo_fee_parcela_reduzida)}
430
+ />
431
+ <DataCard
432
+ label="Deduções Totais"
433
+ value={formatCurrency(resultado.custo_total_deducoes)}
434
+ />
435
+ <DataCard
436
+ label="Cliente Simulado"
437
+ value={resultado.fluxo_cliente.client_name}
438
+ subvalue={resultado.fluxo_cliente.client_id}
439
+ />
440
+ </div>
441
+
442
+ {"modo_conversao" in resultado && (
443
+ <Card title="Métricas do Modo Conversão">
444
+ <div
445
+ style={{
446
+ display: "grid",
447
+ gridTemplateColumns: "repeat(4, minmax(160px, 1fr))",
448
+ gap: "12px",
449
+ }}
450
+ >
451
+ <DataCard
452
+ label="Valor Imóvel Base"
453
+ value={formatCurrency(resultado.modo_conversao.valor_imovel_base)}
454
+ />
455
+ <DataCard
456
+ label="Valor com Desconto"
457
+ value={formatCurrency(
458
+ resultado.modo_conversao.valor_imovel_com_desconto
459
+ )}
460
+ />
461
+ <DataCard
462
+ label="Entrada Imóvel"
463
+ value={formatCurrency(
464
+ resultado.modo_conversao.valor_entrada_imovel
465
+ )}
466
+ />
467
+ <DataCard
468
+ label="Crédito Consórcio"
469
+ value={formatCurrency(
470
+ resultado.modo_conversao.valor_credito_consorcio
471
+ )}
472
+ />
473
+ <DataCard
474
+ label="Custo Transação"
475
+ value={formatCurrency(
476
+ resultado.modo_conversao.valor_custos_transacao
477
+ )}
478
+ />
479
+ <DataCard
480
+ label="Investimento Conversão"
481
+ value={formatCurrency(
482
+ resultado.modo_conversao.investimento_total_conversao
483
+ )}
484
+ />
485
+ <DataCard
486
+ label="Saída no Secundário"
487
+ value={formatCurrency(
488
+ resultado.modo_conversao.valor_saida_consorcio_secundario
489
+ )}
490
+ />
491
+ <DataCard
492
+ label="Lucro Projetado"
493
+ value={formatCurrency(
494
+ resultado.modo_conversao.lucro_projetado_conversao
495
+ )}
496
+ subvalue={`ROI ${formatPercent(
497
+ resultado.modo_conversao.roi_projetado_conversao
498
+ )}`}
499
+ />
500
+ </div>
501
+ </Card>
502
+ )}
503
+
504
+ <Table>
505
+ <TableHead>
506
+ <TableRow>
507
+ <TableHeader>Período</TableHeader>
508
+ <TableHeader style={{ textAlign: "right" }}>
509
+ Fluxo (R$)
510
+ </TableHeader>
511
+ </TableRow>
512
+ </TableHead>
513
+ <TableBody>
514
+ {(resumoFluxo || []).map((point) => (
515
+ <TableRow key={`${point.periodo}-${point.fluxo}`}>
516
+ <TableCell>{point.periodo}</TableCell>
517
+ <TableCell style={{ textAlign: "right", fontFamily: "monospace" }}>
518
+ {formatCurrency(point.fluxo)}
519
+ </TableCell>
520
+ </TableRow>
521
+ ))}
522
+ </TableBody>
523
+ </Table>
524
+ </div>
525
+ ) : (
526
+ <div style={{ padding: "20px", color: "var(--text-muted)" }}>
527
+ Selecione um cliente/padrão e clique em <strong>Simular Cenário</strong>.
528
+ </div>
529
+ )}
530
+ </Card>
531
+ </div>
532
+ </div>
533
+ );
534
+ }
apps/web_comercial/src/app/page.tsx ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { SimulatorForm } from "@/components/simulator/SimulatorForm";
2
+ import { DataCard } from "@/components/ui/Card";
3
+
4
+ export default function Home() {
5
+ return (
6
+ <div className="module-container">
7
+ <div className="module-header">
8
+ <h1 className="module-title">Simulador de Consórcio Imobiliário</h1>
9
+ <p className="module-subtitle">
10
+ Estrutura operacional inspirada na planilha Rodobens/BTG, com leitura
11
+ tabular, resumo executivo e preparação de proposta cliente.
12
+ </p>
13
+ </div>
14
+
15
+ <div className="page-kpi-strip">
16
+ <DataCard
17
+ label="Escopo"
18
+ value="Somente imóvel"
19
+ subvalue="Produto bloqueado para carteira imobiliária"
20
+ />
21
+ <DataCard
22
+ label="Base operacional"
23
+ value="Workbook Rodobens"
24
+ subvalue="Degrau, Linear, Pontual e variantes reduzidas"
25
+ highlight
26
+ />
27
+ <DataCard
28
+ label="Modo"
29
+ value="Operacional"
30
+ subvalue="Resumo geral, CET e memória de cálculo"
31
+ />
32
+ <DataCard
33
+ label="Status"
34
+ value="Pronto para validação"
35
+ subvalue="Comparador, proposta, premissas e configurações ativos"
36
+ />
37
+ </div>
38
+
39
+ <div className="executive-banner">
40
+ <div>
41
+ <strong>Leitura de planilha convertida em software.</strong> O foco
42
+ aqui não é marketing; é velocidade operacional, legibilidade numérica
43
+ e confiança para originar proposta.
44
+ </div>
45
+ <div className="executive-banner__meta">
46
+ <span className="status-chip status-live">Motor determinístico</span>
47
+ <span className="metric-chip">Resumo geral ativo</span>
48
+ <span className="metric-chip">CET e ao mês</span>
49
+ </div>
50
+ </div>
51
+
52
+ <div className="module-content">
53
+ <SimulatorForm />
54
+ </div>
55
+ </div>
56
+ );
57
+ }
apps/web_comercial/src/app/premissas/page.tsx ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Card, DataCard } from "@/components/ui/Card";
2
+ import { Input, Select } from "@/components/ui/FormControls";
3
+
4
+ export default function PremissasPage() {
5
+ return (
6
+ <div className="module-container">
7
+ <div className="module-header">
8
+ <h1 className="module-title">Premissas Operacionais</h1>
9
+ <p className="module-subtitle">
10
+ Parametrização central de regras, metas e datas que alimentam o motor
11
+ determinístico.
12
+ </p>
13
+ </div>
14
+
15
+ <div className="page-kpi-strip">
16
+ <DataCard label="Objetivo" value="Governança do cálculo" subvalue="Hipóteses macro e políticas de operação" />
17
+ <DataCard label="Critério" value="Payback e retorno" subvalue="Premissas usadas na leitura executiva" highlight />
18
+ <DataCard label="Impacto" value="Todas as rotas" subvalue="Simulador, comparador e proposta" />
19
+ </div>
20
+
21
+ <div
22
+ className="module-content"
23
+ style={{ display: "flex", flexDirection: "column", gap: "24px" }}
24
+ >
25
+ <Card title="Hipóteses Macroeconômicas e Objetivos">
26
+ <div
27
+ style={{
28
+ display: "grid",
29
+ gridTemplateColumns: "1fr 1fr 1fr",
30
+ gap: "16px",
31
+ }}
32
+ >
33
+ <Input label="Retorno Alvo Anual" defaultValue={15} suffix="%" />
34
+ <Input label="Custo de Portabilidade" defaultValue={0} suffix="%" />
35
+ <Select
36
+ label="Objetivo da Operação"
37
+ options={[
38
+ { label: "Formação de Patrimônio", value: "patrimonio" },
39
+ { label: "Alavancagem", value: "alavancagem" },
40
+ ]}
41
+ />
42
+ <Select
43
+ label="Regra de Reajuste"
44
+ options={[
45
+ { label: "INCC + IGP-M Anual", value: "incc" },
46
+ { label: "Fixo", value: "fixo" },
47
+ ]}
48
+ />
49
+ <Input label="Payback Desejado" defaultValue={48} suffix=" Meses" />
50
+ </div>
51
+ </Card>
52
+
53
+ <Card title="Eventos e Faturamento">
54
+ <div
55
+ style={{
56
+ display: "grid",
57
+ gridTemplateColumns: "1fr 1fr",
58
+ gap: "16px",
59
+ }}
60
+ >
61
+ <Input label="Data do Próximo Sorteio" type="date" />
62
+ <Input label="Data da Próxima AGE" type="date" />
63
+ <Input label="Último Lance Vencedor" defaultValue={32} suffix="%" />
64
+ <Select
65
+ label="Política para Não Contemplados"
66
+ options={[
67
+ { label: "Liquidar no secundário em 12m", value: "12m" },
68
+ { label: "Manter até o fim", value: "fim" },
69
+ ]}
70
+ />
71
+ </div>
72
+ </Card>
73
+
74
+ <div
75
+ style={{
76
+ display: "flex",
77
+ justifyContent: "flex-end",
78
+ gap: "12px",
79
+ marginTop: "16px",
80
+ }}
81
+ >
82
+ <button className="btn btn-secondary">Descartar Alterações</button>
83
+ <button className="btn btn-primary">Salvar Premissas</button>
84
+ </div>
85
+ </div>
86
+ </div>
87
+ );
88
+ }
apps/web_comercial/src/app/proposta/page.tsx ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { DataCard } from "@/components/ui/Card";
4
+ import {
5
+ Table,
6
+ TableBody,
7
+ TableCell,
8
+ TableHead,
9
+ TableHeader,
10
+ TableRow,
11
+ } from "@/components/ui/Table";
12
+ import { calcularSimulacao } from "@simulador-consorcio/simulation-engine";
13
+ import { useMemo } from "react";
14
+
15
+ function formatCurrency(value: number) {
16
+ return new Intl.NumberFormat("pt-BR", {
17
+ style: "currency",
18
+ currency: "BRL",
19
+ }).format(value);
20
+ }
21
+
22
+ export default function PropostaPage() {
23
+ // Configuração Fixa Simulada para a Proposta Executiva
24
+ const propostaData = useMemo(() => {
25
+ return calcularSimulacao({
26
+ credito_desejado: 500000,
27
+ parametros: {
28
+ id_plano: "PADRAO",
29
+ nome_plano: "Plano Padrão Imóvel",
30
+ prazo_meses: 180,
31
+ taxa_administracao_total: 0.15,
32
+ fundo_reserva_mensal: 0.0005,
33
+ seguro_prestamista: 0.035,
34
+ },
35
+ tem_lance: true,
36
+ lance_percentual: 0.3,
37
+ });
38
+ }, []);
39
+
40
+ return (
41
+ <div className="module-container" style={{ maxWidth: "960px", margin: "0 auto" }}>
42
+ <div className="page-kpi-strip">
43
+ <DataCard label="Formato" value="Proposta cliente" subvalue="Documento financeiro compacto" />
44
+ <DataCard label="Base" value="Simulação determinística" subvalue="Valores derivados do motor atual" highlight />
45
+ <DataCard label="Uso" value="PDF / reunião" subvalue="Leitura mais limpa que a visão operacional" />
46
+ </div>
47
+
48
+ <div className="sheet-frame">
49
+ <div
50
+ className="module-header"
51
+ style={{
52
+ borderBottom: "2px solid var(--color-brand-dark)",
53
+ padding: "32px 32px 16px",
54
+ marginBottom: 0,
55
+ }}
56
+ >
57
+ <h1
58
+ className="module-title"
59
+ style={{
60
+ textAlign: "center",
61
+ color: "var(--color-brand-dark)",
62
+ textTransform: "uppercase",
63
+ letterSpacing: "1px",
64
+ }}
65
+ >
66
+ Proposta Personalizada
67
+ </h1>
68
+ <p className="module-subtitle" style={{ textAlign: "center" }}>
69
+ Apex Consórcios - Especialistas em Patrimônio
70
+ </p>
71
+ </div>
72
+
73
+ <div
74
+ className="module-content"
75
+ style={{
76
+ padding: "32px",
77
+ backgroundColor: "transparent",
78
+ border: "none",
79
+ boxShadow: "none",
80
+ }}
81
+ >
82
+ <div style={{ marginBottom: "32px" }}>
83
+ <h2
84
+ style={{
85
+ fontSize: "1.2rem",
86
+ color: "var(--color-brand-accent)",
87
+ borderBottom: "1px solid var(--border-color)",
88
+ paddingBottom: "8px",
89
+ marginBottom: "16px",
90
+ }}
91
+ >
92
+ Dados Gerais
93
+ </h2>
94
+ <div
95
+ style={{
96
+ display: "grid",
97
+ gridTemplateColumns: "1fr 1fr",
98
+ gap: "16px",
99
+ }}
100
+ >
101
+ <DataCard label="Cliente" value="João da Silva" />
102
+ <DataCard label="Especialista" value="Maria Oliveira" />
103
+ <DataCard
104
+ label="Data da Proposta"
105
+ value={new Date().toLocaleDateString("pt-BR")}
106
+ />
107
+ <DataCard label="Validade" value="15 Dias" />
108
+ </div>
109
+ </div>
110
+
111
+ <div style={{ marginBottom: "32px" }}>
112
+ <h2
113
+ style={{
114
+ fontSize: "1.2rem",
115
+ color: "var(--color-brand-accent)",
116
+ borderBottom: "1px solid var(--border-color)",
117
+ paddingBottom: "8px",
118
+ marginBottom: "16px",
119
+ }}
120
+ >
121
+ Plano Recomendado: Imóvel Padrão Linear
122
+ </h2>
123
+ <Table>
124
+ <TableHead>
125
+ <TableRow>
126
+ <TableHeader>Parâmetro</TableHeader>
127
+ <TableHeader style={{ textAlign: "right" }}>Valor</TableHeader>
128
+ </TableRow>
129
+ </TableHead>
130
+ <TableBody>
131
+ <TableRow>
132
+ <TableCell>Crédito Total</TableCell>
133
+ <TableCell style={{ textAlign: "right", fontWeight: 600 }}>
134
+ {formatCurrency(propostaData.credito)}
135
+ </TableCell>
136
+ </TableRow>
137
+ <TableRow>
138
+ <TableCell>Prazo Total</TableCell>
139
+ <TableCell style={{ textAlign: "right" }}>
140
+ {propostaData.prazo} Meses
141
+ </TableCell>
142
+ </TableRow>
143
+ <TableRow>
144
+ <TableCell>Parcela Base (S/ Redução)</TableCell>
145
+ <TableCell
146
+ style={{
147
+ textAlign: "right",
148
+ color: "var(--color-brand-accent)",
149
+ fontWeight: 600,
150
+ }}
151
+ >
152
+ {formatCurrency(propostaData.parcela_integral.total)}
153
+ </TableCell>
154
+ </TableRow>
155
+ <TableRow>
156
+ <TableCell>Taxa de Administração Total</TableCell>
157
+ <TableCell style={{ textAlign: "right" }}>15,00%</TableCell>
158
+ </TableRow>
159
+ <TableRow>
160
+ <TableCell>Fundo de Reserva</TableCell>
161
+ <TableCell style={{ textAlign: "right" }}>Mensal</TableCell>
162
+ </TableRow>
163
+ </TableBody>
164
+ </Table>
165
+ </div>
166
+
167
+ <div style={{ marginBottom: "32px" }}>
168
+ <h2
169
+ style={{
170
+ fontSize: "1.2rem",
171
+ color: "var(--color-brand-accent)",
172
+ borderBottom: "1px solid var(--border-color)",
173
+ paddingBottom: "8px",
174
+ marginBottom: "16px",
175
+ }}
176
+ >
177
+ Estimativa Pós Contemplação (Lance 30%)
178
+ </h2>
179
+ <div
180
+ style={{
181
+ display: "grid",
182
+ gridTemplateColumns: "1fr 1fr 1fr",
183
+ gap: "16px",
184
+ }}
185
+ >
186
+ <DataCard
187
+ label="Lance Embutido Estimado"
188
+ value={formatCurrency(propostaData.valor_lance || 0)}
189
+ />
190
+ <DataCard
191
+ label="Crédito Disponível Líquido"
192
+ value={formatCurrency(
193
+ propostaData.credito - (propostaData.valor_lance || 0),
194
+ )}
195
+ highlight
196
+ />
197
+ <DataCard
198
+ label="Prazo Restante Estimado"
199
+ value={`${propostaData.prazo_apos_lance || propostaData.prazo} Meses`}
200
+ />
201
+ </div>
202
+ </div>
203
+
204
+ <div>
205
+ <h2
206
+ style={{
207
+ fontSize: "1.2rem",
208
+ color: "var(--color-brand-accent)",
209
+ borderBottom: "1px solid var(--border-color)",
210
+ paddingBottom: "8px",
211
+ marginBottom: "16px",
212
+ }}
213
+ >
214
+ Informações Adicionais
215
+ </h2>
216
+ <p
217
+ style={{
218
+ fontSize: "0.85rem",
219
+ color: "var(--text-muted)",
220
+ lineHeight: 1.5,
221
+ }}
222
+ >
223
+ Os valores apresentados acima representam uma estimativa
224
+ determinística de cenário futuro e não constituem garantia de
225
+ contemplação. O custo efetivo total não considera reajustes
226
+ monetários aplicáveis conforme contrato. A taxa administrativa já
227
+ engloba o período selecionado de parcelamento. A viabilidade é
228
+ baseada nas regras operacionais vigentes.
229
+ </p>
230
+ </div>
231
+ </div>
232
+ </div>
233
+ </div>
234
+ );
235
+ }
apps/web_comercial/src/components/layout/AppLayout.tsx ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { Sidebar } from "./Sidebar";
3
+ import { Topbar } from "./Topbar";
4
+
5
+ interface AppLayoutProps {
6
+ children: React.ReactNode;
7
+ }
8
+
9
+ export function AppLayout({ children }: AppLayoutProps) {
10
+ return (
11
+ <div className="app-container">
12
+ <Sidebar />
13
+ <div className="app-main">
14
+ <Topbar />
15
+ <main className="app-content">{children}</main>
16
+ </div>
17
+ </div>
18
+ );
19
+ }
apps/web_comercial/src/components/layout/Sidebar.tsx ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { usePathname } from "next/navigation";
5
+
6
+ const navigationItems = [
7
+ { href: "/", label: "Simulador", meta: "Operacional" },
8
+ {
9
+ href: "/mercado-secundario",
10
+ label: "Mercado Secundário",
11
+ meta: "Portfólio",
12
+ },
13
+ { href: "/comparador", label: "Comparador", meta: "Cenários" },
14
+ { href: "/proposta", label: "Proposta", meta: "Cliente" },
15
+ { href: "/premissas", label: "Premissas", meta: "Regras" },
16
+ { href: "/configuracoes", label: "Configurações", meta: "Modos" },
17
+ ];
18
+
19
+ export function Sidebar() {
20
+ const pathname = usePathname();
21
+
22
+ return (
23
+ <aside className="app-sidebar">
24
+ <div className="sidebar-header">
25
+ <p className="sidebar-kicker">Base Rodobens Imóvel</p>
26
+ <div className="logos">
27
+ <div className="logo-lockup">
28
+ <div className="logo-placeholder btg-style">BTG Pactual</div>
29
+ <span className="logo-caption">
30
+ Camada comercial e governança executiva.
31
+ </span>
32
+ </div>
33
+ <div className="logo-lockup">
34
+ <div className="logo-placeholder rodobens-style">Rodobens</div>
35
+ <span className="logo-caption">
36
+ Tabelas, premissas e padrões operacionais da carteira imobiliária.
37
+ </span>
38
+ </div>
39
+ </div>
40
+ </div>
41
+
42
+ <nav className="sidebar-nav">
43
+ <div className="sidebar-section-label">Módulos</div>
44
+ <ul>
45
+ {navigationItems.map((item) => {
46
+ const isActive =
47
+ item.href === "/"
48
+ ? pathname === item.href
49
+ : pathname?.startsWith(item.href);
50
+
51
+ return (
52
+ <li key={item.href}>
53
+ <Link
54
+ href={item.href}
55
+ className={`nav-link ${isActive ? "active" : ""}`}
56
+ >
57
+ <span>{item.label}</span>
58
+ <span className="nav-link__meta">{item.meta}</span>
59
+ </Link>
60
+ </li>
61
+ );
62
+ })}
63
+ </ul>
64
+ </nav>
65
+
66
+ <div className="sidebar-footer">
67
+ <strong>Workspace operacional</strong>
68
+ <span>Simulador interno, proposta cliente e mercado secundário.</span>
69
+ </div>
70
+ </aside>
71
+ );
72
+ }
apps/web_comercial/src/components/layout/Topbar.tsx ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { usePathname } from "next/navigation";
4
+
5
+ const routeConfig: Record<
6
+ string,
7
+ { title: string; subtitle: string; validity: string }
8
+ > = {
9
+ "/": {
10
+ title: "Simulador imobiliário",
11
+ subtitle: "Montagem de proposta com leitura operacional em formato workbook.",
12
+ validity: "Vigência 31/10/2026",
13
+ },
14
+ "/mercado-secundario": {
15
+ title: "Mercado secundário",
16
+ subtitle: "Análise de fluxo, portabilidade, spread e retorno por cliente SCP.",
17
+ validity: "Ciclo março/2026",
18
+ },
19
+ "/comparador": {
20
+ title: "Comparador de cenários",
21
+ subtitle: "Confronto entre planos, prazo, lance e impacto de parcela.",
22
+ validity: "Revisão diária",
23
+ },
24
+ "/proposta": {
25
+ title: "Proposta personalizada",
26
+ subtitle: "Documento executivo orientado a PDF e apresentação ao cliente.",
27
+ validity: "Validade 15 dias",
28
+ },
29
+ "/premissas": {
30
+ title: "Premissas operacionais",
31
+ subtitle: "Hipóteses, gatilhos e regras que sustentam o cálculo determinístico.",
32
+ validity: "Baseline homologada",
33
+ },
34
+ "/configuracoes": {
35
+ title: "Configurações",
36
+ subtitle: "Modos de leitura, densidade e visibilidade de memória de cálculo.",
37
+ validity: "Perfil operacional",
38
+ },
39
+ };
40
+
41
+ export function Topbar() {
42
+ const pathname = usePathname();
43
+ const config =
44
+ routeConfig[pathname ?? "/"] ?? routeConfig["/"];
45
+
46
+ return (
47
+ <header className="app-topbar">
48
+ <div className="topbar-heading">
49
+ <span className="topbar-kicker">Consórcio imobiliário Rodobens</span>
50
+ <h1 className="topbar-title">{config.title}</h1>
51
+ <p className="topbar-subtitle">{config.subtitle}</p>
52
+ </div>
53
+
54
+ <div style={{ display: "grid", gap: "10px", justifyItems: "end" }}>
55
+ <div className="topbar-context">
56
+ <span className="context-pill">Parceiro Apex</span>
57
+ <span className="context-pill">Modo operacional</span>
58
+ <span className="context-pill highlight">BTG + Rodobens</span>
59
+ <span className="context-pill">{config.validity}</span>
60
+ </div>
61
+ <div className="topbar-actions">
62
+ <button className="btn btn-secondary">Visualizar layout</button>
63
+ <button className="btn btn-primary">Imprimir proposta</button>
64
+ </div>
65
+ </div>
66
+ </header>
67
+ );
68
+ }
apps/web_comercial/src/components/layout/layout.css ADDED
@@ -0,0 +1,350 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .app-container {
2
+ display: flex;
3
+ min-height: 100vh;
4
+ background: var(--background);
5
+ }
6
+
7
+ .app-sidebar {
8
+ width: 276px;
9
+ flex-shrink: 0;
10
+ display: flex;
11
+ flex-direction: column;
12
+ background:
13
+ linear-gradient(180deg, rgba(255, 255, 255, 0.04), transparent 28%),
14
+ var(--bg-sidebar);
15
+ color: #f8fbff;
16
+ border-right: 1px solid rgba(255, 255, 255, 0.06);
17
+ box-shadow: 12px 0 28px rgba(9, 22, 38, 0.12);
18
+ }
19
+
20
+ .sidebar-header {
21
+ padding: 26px 24px 22px;
22
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
23
+ }
24
+
25
+ .sidebar-kicker {
26
+ margin: 0 0 18px;
27
+ color: rgba(255, 255, 255, 0.56);
28
+ font-size: 0.72rem;
29
+ font-weight: 700;
30
+ letter-spacing: 0.14em;
31
+ text-transform: uppercase;
32
+ }
33
+
34
+ .logos {
35
+ display: flex;
36
+ flex-direction: column;
37
+ gap: 14px;
38
+ }
39
+
40
+ .logo-lockup {
41
+ display: grid;
42
+ gap: 6px;
43
+ }
44
+
45
+ .logo-placeholder {
46
+ display: inline-flex;
47
+ align-items: center;
48
+ width: fit-content;
49
+ min-height: 34px;
50
+ padding: 0 12px;
51
+ border-radius: 8px;
52
+ border: 1px solid rgba(255, 255, 255, 0.12);
53
+ font-weight: 700;
54
+ letter-spacing: 0.05em;
55
+ font-size: 0.82rem;
56
+ text-transform: uppercase;
57
+ }
58
+
59
+ .btg-style {
60
+ color: #ffffff;
61
+ background: rgba(255, 255, 255, 0.06);
62
+ }
63
+
64
+ .rodobens-style {
65
+ color: #c5d4e3;
66
+ background: rgba(255, 255, 255, 0.03);
67
+ }
68
+
69
+ .logo-caption {
70
+ color: rgba(255, 255, 255, 0.62);
71
+ font-size: 0.78rem;
72
+ line-height: 1.45;
73
+ }
74
+
75
+ .sidebar-nav {
76
+ padding: 18px 16px;
77
+ flex: 1;
78
+ }
79
+
80
+ .sidebar-section-label {
81
+ padding: 0 10px 10px;
82
+ color: rgba(255, 255, 255, 0.44);
83
+ font-size: 0.69rem;
84
+ font-weight: 700;
85
+ letter-spacing: 0.16em;
86
+ text-transform: uppercase;
87
+ }
88
+
89
+ .sidebar-nav ul {
90
+ list-style: none;
91
+ padding: 0;
92
+ margin: 0;
93
+ display: flex;
94
+ flex-direction: column;
95
+ gap: 6px;
96
+ }
97
+
98
+ .nav-link {
99
+ display: flex;
100
+ align-items: center;
101
+ justify-content: space-between;
102
+ gap: 12px;
103
+ min-height: 48px;
104
+ padding: 0 14px;
105
+ border-radius: 10px;
106
+ color: rgba(245, 249, 255, 0.76);
107
+ text-decoration: none;
108
+ font-weight: 600;
109
+ font-size: 0.93rem;
110
+ border: 1px solid transparent;
111
+ transition: all 0.18s ease;
112
+ }
113
+
114
+ .nav-link:hover {
115
+ background: rgba(255, 255, 255, 0.05);
116
+ border-color: rgba(255, 255, 255, 0.08);
117
+ color: #ffffff;
118
+ }
119
+
120
+ .nav-link.active {
121
+ background: linear-gradient(90deg, rgba(236, 91, 19, 0.18), rgba(236, 91, 19, 0.06));
122
+ border-color: rgba(236, 91, 19, 0.26);
123
+ color: #ffffff;
124
+ box-shadow: inset 3px 0 0 var(--color-brand-accent);
125
+ }
126
+
127
+ .nav-link__meta {
128
+ color: rgba(255, 255, 255, 0.4);
129
+ font-size: 0.72rem;
130
+ font-weight: 700;
131
+ letter-spacing: 0.08em;
132
+ text-transform: uppercase;
133
+ }
134
+
135
+ .nav-link.active .nav-link__meta {
136
+ color: rgba(255, 255, 255, 0.72);
137
+ }
138
+
139
+ .sidebar-footer {
140
+ padding: 18px 24px 24px;
141
+ border-top: 1px solid rgba(255, 255, 255, 0.08);
142
+ display: grid;
143
+ gap: 8px;
144
+ }
145
+
146
+ .sidebar-footer strong {
147
+ font-size: 0.82rem;
148
+ color: #ffffff;
149
+ }
150
+
151
+ .sidebar-footer span {
152
+ font-size: 0.76rem;
153
+ color: rgba(255, 255, 255, 0.56);
154
+ }
155
+
156
+ .app-main {
157
+ display: flex;
158
+ flex-direction: column;
159
+ flex: 1;
160
+ min-width: 0;
161
+ }
162
+
163
+ .app-topbar {
164
+ position: sticky;
165
+ top: 0;
166
+ z-index: 20;
167
+ display: flex;
168
+ justify-content: space-between;
169
+ align-items: flex-start;
170
+ gap: 18px;
171
+ padding: 22px 28px 18px;
172
+ background: rgba(243, 240, 235, 0.92);
173
+ backdrop-filter: blur(10px);
174
+ border-bottom: 1px solid rgba(197, 188, 174, 0.9);
175
+ }
176
+
177
+ .topbar-heading {
178
+ display: grid;
179
+ gap: 6px;
180
+ }
181
+
182
+ .topbar-kicker {
183
+ color: var(--text-muted);
184
+ font-size: 0.72rem;
185
+ font-weight: 700;
186
+ letter-spacing: 0.14em;
187
+ text-transform: uppercase;
188
+ }
189
+
190
+ .topbar-title {
191
+ margin: 0;
192
+ font-size: 1.35rem;
193
+ font-weight: 700;
194
+ color: var(--color-brand-dark);
195
+ }
196
+
197
+ .topbar-subtitle {
198
+ margin: 0;
199
+ color: var(--text-muted);
200
+ font-size: 0.86rem;
201
+ }
202
+
203
+ .topbar-context {
204
+ display: flex;
205
+ flex-wrap: wrap;
206
+ gap: 8px;
207
+ align-items: center;
208
+ }
209
+
210
+ .context-pill {
211
+ display: inline-flex;
212
+ align-items: center;
213
+ min-height: 30px;
214
+ padding: 0 10px;
215
+ border-radius: 999px;
216
+ background: var(--bg-panel);
217
+ border: 1px solid var(--border-color);
218
+ color: var(--text-main);
219
+ font-size: 0.74rem;
220
+ font-weight: 700;
221
+ letter-spacing: 0.04em;
222
+ text-transform: uppercase;
223
+ box-shadow: var(--shadow-soft);
224
+ }
225
+
226
+ .context-pill.highlight {
227
+ background: rgba(236, 91, 19, 0.1);
228
+ border-color: rgba(236, 91, 19, 0.24);
229
+ color: var(--color-brand-accent-strong);
230
+ }
231
+
232
+ .topbar-actions {
233
+ display: flex;
234
+ align-items: center;
235
+ gap: 10px;
236
+ flex-wrap: wrap;
237
+ }
238
+
239
+ .app-content {
240
+ padding: 28px;
241
+ }
242
+
243
+ .btn {
244
+ display: inline-flex;
245
+ align-items: center;
246
+ justify-content: center;
247
+ min-height: 40px;
248
+ padding: 0 16px;
249
+ border-radius: 10px;
250
+ cursor: pointer;
251
+ transition: all 0.18s ease;
252
+ border: 1px solid transparent;
253
+ font-family: var(--font-sans);
254
+ font-size: 0.82rem;
255
+ font-weight: 700;
256
+ letter-spacing: 0.04em;
257
+ text-transform: uppercase;
258
+ }
259
+
260
+ .btn-primary {
261
+ background: var(--color-brand-accent);
262
+ color: white;
263
+ box-shadow: 0 8px 18px rgba(236, 91, 19, 0.18);
264
+ }
265
+
266
+ .btn-primary:hover {
267
+ background: var(--color-brand-accent-strong);
268
+ }
269
+
270
+ .btn-secondary {
271
+ background: var(--bg-panel);
272
+ color: var(--color-brand-dark);
273
+ border-color: var(--border-color);
274
+ }
275
+
276
+ .btn-secondary:hover {
277
+ background: var(--bg-panel-soft);
278
+ border-color: var(--border-strong);
279
+ }
280
+
281
+ .module-container {
282
+ display: flex;
283
+ flex-direction: column;
284
+ gap: 20px;
285
+ min-width: 0;
286
+ }
287
+
288
+ .module-header {
289
+ display: flex;
290
+ flex-wrap: wrap;
291
+ justify-content: space-between;
292
+ align-items: flex-start;
293
+ gap: 16px;
294
+ }
295
+
296
+ .module-title {
297
+ margin: 0 0 6px;
298
+ font-size: 1.55rem;
299
+ font-weight: 700;
300
+ color: var(--color-brand-dark);
301
+ letter-spacing: -0.02em;
302
+ }
303
+
304
+ .module-subtitle {
305
+ margin: 0;
306
+ color: var(--text-muted);
307
+ font-size: 0.92rem;
308
+ line-height: 1.55;
309
+ }
310
+
311
+ .module-content {
312
+ background: var(--bg-panel);
313
+ border: 1px solid var(--border-color);
314
+ border-radius: 12px;
315
+ padding: 24px;
316
+ box-shadow: var(--shadow-panel);
317
+ min-width: 0;
318
+ }
319
+
320
+ @media (max-width: 1100px) {
321
+ .app-container {
322
+ flex-direction: column;
323
+ }
324
+
325
+ .app-sidebar {
326
+ width: 100%;
327
+ box-shadow: none;
328
+ }
329
+
330
+ .sidebar-nav ul {
331
+ display: grid;
332
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
333
+ }
334
+ }
335
+
336
+ @media (max-width: 768px) {
337
+ .app-topbar,
338
+ .app-content {
339
+ padding-left: 18px;
340
+ padding-right: 18px;
341
+ }
342
+
343
+ .module-content {
344
+ padding: 18px;
345
+ }
346
+
347
+ .topbar-title {
348
+ font-size: 1.12rem;
349
+ }
350
+ }
apps/web_comercial/src/components/simulator/SimulatorForm.tsx ADDED
@@ -0,0 +1,354 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import {
4
+ calcularSimulacao,
5
+ type ParametrosPlano,
6
+ type ResultadoSimulacao,
7
+ type SimulacaoInput,
8
+ } from "@simulador-consorcio/simulation-engine";
9
+ import { useState } from "react";
10
+ import { Card, DataCard } from "../ui/Card";
11
+ import { Input, Select } from "../ui/FormControls";
12
+ import {
13
+ Table,
14
+ TableBody,
15
+ TableCell,
16
+ TableHead,
17
+ TableHeader,
18
+ TableRow,
19
+ } from "../ui/Table";
20
+
21
+ export function SimulatorForm() {
22
+ const [produto, setProduto] = useState("imovel");
23
+ const [modalidade, setModalidade] = useState("linear_integral");
24
+ const [credito, setCredito] = useState<number>(300000);
25
+ const [prazo, setPrazo] = useState<number>(180);
26
+ const [temLance, setTemLance] = useState<boolean>(false);
27
+ const [lancePercentual, setLancePercentual] = useState<number>(30);
28
+ const [resultado, setResultado] = useState<ResultadoSimulacao | null>(null);
29
+
30
+ const formatCurrency = (value: number) => {
31
+ return new Intl.NumberFormat("pt-BR", {
32
+ style: "currency",
33
+ currency: "BRL",
34
+ }).format(value);
35
+ };
36
+
37
+ const handleSimular = () => {
38
+ const parametros: ParametrosPlano = {
39
+ id_plano: "PADRAO",
40
+ nome_plano: "Plano Padrão Imóvel",
41
+ prazo_meses: prazo,
42
+ taxa_administracao_total: 0.15,
43
+ fundo_reserva_mensal: 0.0005,
44
+ seguro_prestamista: 0.035,
45
+ };
46
+
47
+ const input: SimulacaoInput = {
48
+ credito_desejado: credito,
49
+ parametros,
50
+ tem_lance: temLance,
51
+ lance_percentual: temLance ? lancePercentual / 100 : undefined,
52
+ };
53
+
54
+ const res = calcularSimulacao(input);
55
+ setResultado(res);
56
+ };
57
+
58
+ const modalidadeLabelMap: Record<string, string> = {
59
+ linear_integral: "Linear Integral",
60
+ linear_50: "Linear Reduzido 50%",
61
+ linear_70: "Linear Reduzido 70%",
62
+ degrau: "Degrau",
63
+ degrau_reduzido: "Degrau Reduzido",
64
+ pontual: "Pontual",
65
+ pontual_reduzido: "Pontual Reduzido",
66
+ };
67
+
68
+ const prazoLabel =
69
+ prazo === 216 ? "Longo prazo" : prazo >= 180 ? "Prazo padrão" : "Prazo curto";
70
+
71
+ return (
72
+ <div className="section-grid section-grid--simulator">
73
+ <div className="section-stack">
74
+ <Card
75
+ title="Leitura operacional"
76
+ headerAction={<span className="status-chip status-info">{modalidadeLabelMap[modalidade]}</span>}
77
+ >
78
+ <div className="page-kpi-strip" style={{ marginBottom: 0 }}>
79
+ <DataCard
80
+ label="Produto"
81
+ value="Imóvel"
82
+ subvalue="Escopo exclusivo do simulador"
83
+ />
84
+ <DataCard
85
+ label="Plano selecionado"
86
+ value={modalidadeLabelMap[modalidade]}
87
+ subvalue={prazoLabel}
88
+ />
89
+ <DataCard
90
+ label="Crédito em análise"
91
+ value={formatCurrency(credito)}
92
+ subvalue={`Prazo base ${prazo} meses`}
93
+ highlight
94
+ />
95
+ <DataCard
96
+ label="Lance"
97
+ value={temLance ? `${lancePercentual}%` : "Sem lance"}
98
+ subvalue="Operação editável"
99
+ />
100
+ </div>
101
+ </Card>
102
+
103
+ <Card title="Informações Básicas">
104
+ <div className="field-grid field-grid--2">
105
+ <Select
106
+ label="Tipo Produto"
107
+ value={produto}
108
+ onChange={(e) => setProduto(e.target.value)}
109
+ options={[{ label: "Imóvel", value: "imovel" }]}
110
+ disabled // Locked to 'imovel' per instruction
111
+ />
112
+ <Select
113
+ label="Modalidade"
114
+ value={modalidade}
115
+ onChange={(e) => setModalidade(e.target.value)}
116
+ options={[
117
+ { label: "Linear Integral", value: "linear_integral" },
118
+ { label: "Linear Reduzido 50%", value: "linear_50" },
119
+ { label: "Linear Reduzido 70%", value: "linear_70" },
120
+ { label: "Degrau", value: "degrau" },
121
+ { label: "Degrau Reduzido", value: "degrau_reduzido" },
122
+ { label: "Pontual", value: "pontual" },
123
+ { label: "Pontual Reduzido", value: "pontual_reduzido" },
124
+ ]}
125
+ />
126
+ <Input label="Grupo" placeholder="Ex: 4001" />
127
+ <Input label="Número de Cotas" type="number" defaultValue={1} />
128
+ </div>
129
+ </Card>
130
+
131
+ <Card title="Informações do Plano">
132
+ <div className="field-grid field-grid--2">
133
+ <Input
134
+ label="Valor do Crédito"
135
+ type="number"
136
+ value={credito}
137
+ onChange={(e) => setCredito(Number(e.target.value))}
138
+ prefix="R$"
139
+ />
140
+ <Select
141
+ label="Prazo do Plano"
142
+ value={prazo}
143
+ onChange={(e) => setPrazo(Number(e.target.value))}
144
+ options={[
145
+ { label: "120 meses", value: "120" },
146
+ { label: "150 meses", value: "150" },
147
+ { label: "180 meses", value: "180" },
148
+ { label: "216 meses", value: "216" },
149
+ ]}
150
+ />
151
+ <Input
152
+ label="Taxa de Administração Total"
153
+ defaultValue="15.00"
154
+ suffix="%"
155
+ />
156
+ <Input label="Fundo de Reserva" defaultValue="0.05" suffix="%" />
157
+ <Input label="Seguro Prestamista" defaultValue="3.50" suffix="%" />
158
+ <Input label="Taxa de Adesão" defaultValue="1.00" suffix="%" />
159
+ </div>
160
+ </Card>
161
+
162
+ <Card title="Informações de Lance">
163
+ <div className="field-grid field-grid--2">
164
+ <div className="ui-form-group" style={{ gridColumn: "1 / -1" }}>
165
+ <label className="inline-toggle">
166
+ <input
167
+ type="checkbox"
168
+ checked={temLance}
169
+ onChange={(e) => setTemLance(e.target.checked)}
170
+ />
171
+ Simular contemplação via lance
172
+ </label>
173
+ </div>
174
+ {temLance && (
175
+ <>
176
+ <Select
177
+ label="Tipo de Lance"
178
+ options={[
179
+ { label: "Fixo", value: "fixo" },
180
+ { label: "Livre", value: "livre" },
181
+ ]}
182
+ />
183
+ <Input
184
+ label="Lance Total (%)"
185
+ type="number"
186
+ value={lancePercentual}
187
+ onChange={(e) => setLancePercentual(Number(e.target.value))}
188
+ suffix="%"
189
+ />
190
+ <Input
191
+ label="Embutido Percentual"
192
+ type="number"
193
+ defaultValue={10}
194
+ suffix="%"
195
+ tooltip="Percentual do lance a ser retirado do próprio crédito."
196
+ />
197
+ <Select
198
+ label="Mês Previsto de Lance"
199
+ options={Array.from({ length: 24 }, (_, i) => ({
200
+ label: `Mês ${i + 1}`,
201
+ value: i + 1,
202
+ }))}
203
+ />
204
+ </>
205
+ )}
206
+ </div>
207
+
208
+ <div style={{ marginTop: "20px", display: "flex", justifyContent: "flex-end", gap: "12px" }}>
209
+ <button className="btn btn-secondary">Limpar</button>
210
+ <button className="btn btn-primary" onClick={handleSimular}>
211
+ Calcular Cenário
212
+ </button>
213
+ </div>
214
+ </Card>
215
+ </div>
216
+
217
+ <div className="summary-rail">
218
+ <Card title="Resumo Geral" highlight={true}>
219
+ {resultado ? (
220
+ <div className="section-stack" style={{ gap: "12px" }}>
221
+ <DataCard
222
+ label="Crédito Disponível"
223
+ value={formatCurrency(resultado.credito || credito)}
224
+ />
225
+ <DataCard
226
+ label="Parcela Inicial (Integral)"
227
+ value={formatCurrency(resultado.parcela_integral.total)}
228
+ highlight
229
+ />
230
+ {resultado.parcela_reduzida && (
231
+ <DataCard
232
+ label="Parcela Reduzida"
233
+ value={formatCurrency(resultado.parcela_reduzida.total)}
234
+ />
235
+ )}
236
+ {temLance && resultado.valor_lance && (
237
+ <>
238
+ <DataCard
239
+ label="Valor Total do Lance"
240
+ value={formatCurrency(resultado.valor_lance)}
241
+ />
242
+ <DataCard
243
+ label="Prazo Restante"
244
+ value={`${resultado.prazo_apos_lance} meses`}
245
+ />
246
+ </>
247
+ )}
248
+ </div>
249
+ ) : (
250
+ <p className="plain-note">
251
+ Preencha os blocos operacionais para liberar o resumo, o CET e a
252
+ composição da parcela.
253
+ </p>
254
+ )}
255
+ </Card>
256
+
257
+ <Card title="CET / Ao mês">
258
+ {resultado ? (
259
+ <div className="section-stack" style={{ gap: "12px" }}>
260
+ <DataCard
261
+ label="Custo Efetivo Total (Ao Ano)"
262
+ value="11.45%"
263
+ subvalue="Estimativa com base nas taxas atuais"
264
+ />
265
+ <DataCard label="Taxa Equivalente (Ao Mês)" value="0.91%" />
266
+ </div>
267
+ ) : (
268
+ <p className="plain-note">Aguardando geração do cenário.</p>
269
+ )}
270
+ </Card>
271
+
272
+ <Card title="Checklist operacional">
273
+ <div className="section-stack" style={{ gap: "10px" }}>
274
+ <div className="executive-banner" style={{ padding: "12px 14px" }}>
275
+ <div>
276
+ <strong>Validação de escopo:</strong> produto travado em imóvel e
277
+ modalidades alinhadas ao workbook.
278
+ </div>
279
+ </div>
280
+ <p className="plain-note">
281
+ Antes de emitir proposta, confirme grupo, prazo, lance, CET, faixa
282
+ de parcela e crédito líquido disponível.
283
+ </p>
284
+ </div>
285
+ </Card>
286
+
287
+ {resultado && (
288
+ <Card title="Composição da Parcela" noPadding>
289
+ <Table>
290
+ <TableHead>
291
+ <TableRow>
292
+ <TableHeader>Rubrica</TableHeader>
293
+ <TableHeader style={{ textAlign: "right" }}>
294
+ Valor (R$)
295
+ </TableHeader>
296
+ </TableRow>
297
+ </TableHead>
298
+ <TableBody>
299
+ <TableRow>
300
+ <TableCell>Fundo Comum</TableCell>
301
+ <TableCell
302
+ style={{ textAlign: "right", fontFamily: "monospace" }}
303
+ >
304
+ {formatCurrency(resultado.parcela_integral.fundo_comum)}
305
+ </TableCell>
306
+ </TableRow>
307
+ <TableRow>
308
+ <TableCell>Taxa Administrativa</TableCell>
309
+ <TableCell
310
+ style={{ textAlign: "right", fontFamily: "monospace" }}
311
+ >
312
+ {formatCurrency(
313
+ resultado.parcela_integral.taxa_administracao,
314
+ )}
315
+ </TableCell>
316
+ </TableRow>
317
+ <TableRow>
318
+ <TableCell>Fundo de Reserva</TableCell>
319
+ <TableCell
320
+ style={{ textAlign: "right", fontFamily: "monospace" }}
321
+ >
322
+ {formatCurrency(resultado.parcela_integral.fundo_reserva)}
323
+ </TableCell>
324
+ </TableRow>
325
+ <TableRow>
326
+ <TableCell>Seguro Prestamista</TableCell>
327
+ <TableCell
328
+ style={{ textAlign: "right", fontFamily: "monospace" }}
329
+ >
330
+ {formatCurrency(resultado.parcela_integral.seguro)}
331
+ </TableCell>
332
+ </TableRow>
333
+ <TableRow
334
+ style={{ backgroundColor: "var(--bg-main)", fontWeight: 600 }}
335
+ >
336
+ <TableCell>Total</TableCell>
337
+ <TableCell
338
+ style={{ textAlign: "right", fontFamily: "monospace" }}
339
+ >
340
+ {formatCurrency(resultado.parcela_integral.total)}
341
+ </TableCell>
342
+ </TableRow>
343
+ </TableBody>
344
+ </Table>
345
+ <div className="table-note">
346
+ <span>Leitura resumida da memória de cálculo.</span>
347
+ <span>Base atual: parcela integral do motor determinístico.</span>
348
+ </div>
349
+ </Card>
350
+ )}
351
+ </div>
352
+ </div>
353
+ );
354
+ }
apps/web_comercial/src/components/ui/Card.tsx ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+
3
+ interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
4
+ children: React.ReactNode;
5
+ title?: string;
6
+ headerAction?: React.ReactNode;
7
+ noPadding?: boolean;
8
+ highlight?: boolean;
9
+ }
10
+
11
+ export function Card({
12
+ children,
13
+ title,
14
+ headerAction,
15
+ noPadding = false,
16
+ highlight = false,
17
+ className = "",
18
+ ...props
19
+ }: CardProps) {
20
+ return (
21
+ <div className={`ui-card ${highlight ? "highlight" : ""} ${className}`} {...props}>
22
+ {(title || headerAction) && (
23
+ <div className="ui-card-header">
24
+ {title && <h3 className="ui-card-title">{title}</h3>}
25
+ {headerAction && <div className="ui-card-action">{headerAction}</div>}
26
+ </div>
27
+ )}
28
+ <div className={`ui-card-content ${noPadding ? "no-padding" : ""}`}>
29
+ {children}
30
+ </div>
31
+ </div>
32
+ );
33
+ }
34
+
35
+ export function DataCard({
36
+ label,
37
+ value,
38
+ subvalue,
39
+ highlight = false,
40
+ }: {
41
+ label: string;
42
+ value: string | React.ReactNode;
43
+ subvalue?: string;
44
+ highlight?: boolean;
45
+ }) {
46
+ return (
47
+ <div className={`ui-data-card ${highlight ? "highlight" : ""}`}>
48
+ <span className="ui-data-label">{label}</span>
49
+ <span className="ui-data-value">{value}</span>
50
+ {subvalue && <span className="ui-data-subvalue">{subvalue}</span>}
51
+ </div>
52
+ );
53
+ }
apps/web_comercial/src/components/ui/FormControls.tsx ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { Tooltip } from "./Tooltip";
3
+
4
+ interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
5
+ label: string;
6
+ tooltip?: string;
7
+ error?: string;
8
+ suffix?: string;
9
+ prefix?: string;
10
+ }
11
+
12
+ export function Input({
13
+ label,
14
+ tooltip,
15
+ error,
16
+ suffix,
17
+ prefix,
18
+ className = "",
19
+ ...props
20
+ }: InputProps) {
21
+ return (
22
+ <div className={`ui-form-group ${className}`}>
23
+ <div className="ui-label-wrapper">
24
+ <label className="ui-form-label">{label}</label>
25
+ {tooltip && (
26
+ <Tooltip content={tooltip}>
27
+ <span className="ui-tooltip-icon">?</span>
28
+ </Tooltip>
29
+ )}
30
+ </div>
31
+ <div className="ui-input-wrapper">
32
+ {prefix && <span className="ui-input-prefix">{prefix}</span>}
33
+ <input
34
+ className={`ui-input ${prefix ? "has-prefix" : ""} ${suffix ? "has-suffix" : ""} ${error ? "is-invalid" : ""}`}
35
+ {...props}
36
+ />
37
+ {suffix && <span className="ui-input-suffix">{suffix}</span>}
38
+ </div>
39
+ {error && <span className="ui-form-error">{error}</span>}
40
+ </div>
41
+ );
42
+ }
43
+
44
+ interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {
45
+ label: string;
46
+ tooltip?: string;
47
+ options: { label: string; value: string | number }[];
48
+ }
49
+
50
+ export function Select({
51
+ label,
52
+ tooltip,
53
+ options,
54
+ className = "",
55
+ ...props
56
+ }: SelectProps) {
57
+ return (
58
+ <div className={`ui-form-group ${className}`}>
59
+ <div className="ui-label-wrapper">
60
+ <label className="ui-form-label">{label}</label>
61
+ {tooltip && (
62
+ <Tooltip content={tooltip}>
63
+ <span className="ui-tooltip-icon">?</span>
64
+ </Tooltip>
65
+ )}
66
+ </div>
67
+ <select className="ui-select" {...props}>
68
+ {options.map((opt) => (
69
+ <option key={opt.value} value={opt.value}>
70
+ {opt.label}
71
+ </option>
72
+ ))}
73
+ </select>
74
+ </div>
75
+ );
76
+ }
apps/web_comercial/src/components/ui/Table.tsx ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+
3
+ interface TableProps extends React.TableHTMLAttributes<HTMLTableElement> {
4
+ children: React.ReactNode;
5
+ }
6
+
7
+ export function Table({ children, className = "", ...props }: TableProps) {
8
+ return (
9
+ <div className="ui-table-container">
10
+ <table className={`ui-table ${className}`} {...props}>
11
+ {children}
12
+ </table>
13
+ </div>
14
+ );
15
+ }
16
+
17
+ export function TableHead({ children }: { children: React.ReactNode }) {
18
+ return <thead className="ui-table-head">{children}</thead>;
19
+ }
20
+
21
+ export function TableBody({ children }: { children: React.ReactNode }) {
22
+ return <tbody className="ui-table-body">{children}</tbody>;
23
+ }
24
+
25
+ export function TableRow({
26
+ children,
27
+ className = "",
28
+ ...props
29
+ }: React.HTMLAttributes<HTMLTableRowElement>) {
30
+ return (
31
+ <tr className={`ui-table-row ${className}`} {...props}>
32
+ {children}
33
+ </tr>
34
+ );
35
+ }
36
+
37
+ export function TableHeader({
38
+ children,
39
+ className = "",
40
+ ...props
41
+ }: React.ThHTMLAttributes<HTMLTableHeaderCellElement>) {
42
+ return (
43
+ <th className={`ui-table-header ${className}`} {...props}>
44
+ {children}
45
+ </th>
46
+ );
47
+ }
48
+
49
+ export function TableCell({
50
+ children,
51
+ className = "",
52
+ ...props
53
+ }: React.TdHTMLAttributes<HTMLTableDataCellElement>) {
54
+ return (
55
+ <td className={`ui-table-cell ${className}`} {...props}>
56
+ {children}
57
+ </td>
58
+ );
59
+ }
apps/web_comercial/src/components/ui/Tabs.tsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from "react";
2
+
3
+ interface TabsProps {
4
+ tabs: { id: string; label: string; content: React.ReactNode }[];
5
+ defaultTab?: string;
6
+ }
7
+
8
+ export function Tabs({ tabs, defaultTab }: TabsProps) {
9
+ const [activeTab, setActiveTab] = useState(defaultTab || tabs[0]?.id);
10
+
11
+ return (
12
+ <div className="ui-tabs-container">
13
+ <div className="ui-tabs-header">
14
+ {tabs.map((tab) => (
15
+ <button
16
+ key={tab.id}
17
+ className={`ui-tab-button ${activeTab === tab.id ? "active" : ""}`}
18
+ onClick={() => setActiveTab(tab.id)}
19
+ >
20
+ {tab.label}
21
+ </button>
22
+ ))}
23
+ </div>
24
+ <div className="ui-tabs-content">
25
+ {tabs.find((tab) => tab.id === activeTab)?.content}
26
+ </div>
27
+ </div>
28
+ );
29
+ }
apps/web_comercial/src/components/ui/Tooltip.tsx ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+
3
+ interface TooltipProps {
4
+ content: string;
5
+ children: React.ReactNode;
6
+ }
7
+
8
+ export function Tooltip({ content, children }: TooltipProps) {
9
+ return (
10
+ <div className="ui-tooltip-container">
11
+ {children}
12
+ <div className="ui-tooltip-content">{content}</div>
13
+ </div>
14
+ );
15
+ }
apps/web_comercial/src/components/ui/ui.css ADDED
@@ -0,0 +1,318 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Card */
2
+ .ui-card {
3
+ background-color: var(--bg-panel);
4
+ border: 1px solid var(--border-color);
5
+ border-radius: 10px;
6
+ overflow: hidden;
7
+ display: flex;
8
+ flex-direction: column;
9
+ box-shadow: var(--shadow-soft);
10
+ }
11
+
12
+ .ui-card.highlight {
13
+ border-color: rgba(13, 45, 82, 0.16);
14
+ box-shadow:
15
+ inset 0 1px 0 rgba(255, 255, 255, 0.7),
16
+ 0 10px 22px rgba(13, 45, 82, 0.07);
17
+ }
18
+
19
+ .ui-card-header {
20
+ min-height: 50px;
21
+ padding: 0 18px;
22
+ border-bottom: 1px solid var(--border-color);
23
+ display: flex;
24
+ justify-content: space-between;
25
+ align-items: center;
26
+ background:
27
+ linear-gradient(180deg, rgba(13, 45, 82, 0.04), rgba(13, 45, 82, 0.01)),
28
+ var(--bg-panel-soft);
29
+ }
30
+
31
+ .ui-card-title {
32
+ font-size: 0.88rem;
33
+ font-weight: 700;
34
+ color: var(--color-brand-dark);
35
+ margin: 0;
36
+ letter-spacing: 0.04em;
37
+ text-transform: uppercase;
38
+ }
39
+
40
+ .ui-card-content {
41
+ padding: 18px;
42
+ flex: 1;
43
+ }
44
+
45
+ .ui-card-content.no-padding {
46
+ padding: 0;
47
+ }
48
+
49
+ .ui-data-card {
50
+ display: flex;
51
+ flex-direction: column;
52
+ min-height: 88px;
53
+ padding: 12px 14px;
54
+ border: 1px solid var(--border-color);
55
+ border-radius: 8px;
56
+ background:
57
+ linear-gradient(180deg, rgba(255, 255, 255, 0.9), rgba(248, 245, 241, 0.95)),
58
+ var(--bg-panel-soft);
59
+ }
60
+
61
+ .ui-data-card.highlight {
62
+ background:
63
+ linear-gradient(180deg, rgba(237, 244, 251, 0.95), rgba(223, 237, 248, 0.98)),
64
+ var(--bg-panel-accent);
65
+ border-color: rgba(13, 45, 82, 0.18);
66
+ }
67
+
68
+ .ui-data-label {
69
+ font-size: 0.68rem;
70
+ font-weight: 700;
71
+ color: var(--text-muted);
72
+ text-transform: uppercase;
73
+ letter-spacing: 0.08em;
74
+ margin-bottom: 8px;
75
+ }
76
+
77
+ .ui-data-value {
78
+ font-size: 1.18rem;
79
+ font-weight: 700;
80
+ color: var(--text-main);
81
+ line-height: 1.2;
82
+ }
83
+
84
+ .ui-data-subvalue {
85
+ font-size: 0.75rem;
86
+ color: var(--text-muted);
87
+ margin-top: 4px;
88
+ }
89
+
90
+ /* Form Controls */
91
+ .ui-form-group {
92
+ display: flex;
93
+ flex-direction: column;
94
+ gap: 6px;
95
+ }
96
+
97
+ .ui-label-wrapper {
98
+ display: flex;
99
+ align-items: center;
100
+ gap: 6px;
101
+ }
102
+
103
+ .ui-form-label {
104
+ font-size: 0.74rem;
105
+ font-weight: 700;
106
+ color: var(--text-muted);
107
+ text-transform: uppercase;
108
+ letter-spacing: 0.07em;
109
+ }
110
+
111
+ .ui-tooltip-icon {
112
+ display: inline-flex;
113
+ align-items: center;
114
+ justify-content: center;
115
+ width: 15px;
116
+ height: 15px;
117
+ border-radius: 50%;
118
+ background-color: var(--color-brand-dark);
119
+ color: white;
120
+ font-size: 10px;
121
+ font-weight: 700;
122
+ cursor: help;
123
+ }
124
+
125
+ .ui-input-wrapper {
126
+ position: relative;
127
+ display: flex;
128
+ align-items: center;
129
+ }
130
+
131
+ .ui-input,
132
+ .ui-select {
133
+ width: 100%;
134
+ min-height: 42px;
135
+ padding: 10px 12px;
136
+ font-size: 0.92rem;
137
+ border: 1px solid var(--border-color);
138
+ border-radius: 8px;
139
+ background-color: #fffdfb;
140
+ color: var(--text-main);
141
+ transition:
142
+ border-color 0.18s,
143
+ box-shadow 0.18s,
144
+ background-color 0.18s;
145
+ font-family: inherit;
146
+ }
147
+
148
+ .ui-input:hover,
149
+ .ui-select:hover {
150
+ border-color: var(--border-strong);
151
+ }
152
+
153
+ .ui-input:focus,
154
+ .ui-select:focus {
155
+ outline: none;
156
+ border-color: rgba(13, 45, 82, 0.5);
157
+ box-shadow: 0 0 0 3px rgba(13, 45, 82, 0.08);
158
+ background-color: white;
159
+ }
160
+
161
+ .ui-input.is-invalid {
162
+ border-color: var(--status-negative);
163
+ }
164
+
165
+ .ui-input-prefix,
166
+ .ui-input-suffix {
167
+ position: absolute;
168
+ color: var(--text-muted);
169
+ font-size: 0.86rem;
170
+ }
171
+
172
+ .ui-input-prefix {
173
+ left: 12px;
174
+ }
175
+
176
+ .ui-input-suffix {
177
+ right: 12px;
178
+ }
179
+
180
+ .ui-input.has-prefix {
181
+ padding-left: 34px;
182
+ }
183
+
184
+ .ui-input.has-suffix {
185
+ padding-right: 40px;
186
+ }
187
+
188
+ .ui-form-error {
189
+ color: var(--status-negative);
190
+ font-size: 0.75rem;
191
+ margin-top: 2px;
192
+ }
193
+
194
+ /* Tooltip */
195
+ .ui-tooltip-container {
196
+ position: relative;
197
+ display: inline-flex;
198
+ }
199
+
200
+ .ui-tooltip-content {
201
+ visibility: hidden;
202
+ position: absolute;
203
+ bottom: 150%;
204
+ left: 50%;
205
+ transform: translateX(-50%);
206
+ background-color: #13263e;
207
+ color: white;
208
+ text-align: center;
209
+ padding: 7px 10px;
210
+ border-radius: 6px;
211
+ font-size: 0.74rem;
212
+ white-space: nowrap;
213
+ opacity: 0;
214
+ transition: opacity 0.2s;
215
+ z-index: 100;
216
+ pointer-events: none;
217
+ box-shadow: var(--shadow-soft);
218
+ }
219
+
220
+ .ui-tooltip-content::after {
221
+ content: "";
222
+ position: absolute;
223
+ top: 100%;
224
+ left: 50%;
225
+ margin-left: -5px;
226
+ border-width: 5px;
227
+ border-style: solid;
228
+ border-color: #13263e transparent transparent transparent;
229
+ }
230
+
231
+ .ui-tooltip-container:hover .ui-tooltip-content {
232
+ visibility: visible;
233
+ opacity: 1;
234
+ }
235
+
236
+ /* Table */
237
+ .ui-table-container {
238
+ width: 100%;
239
+ overflow-x: auto;
240
+ }
241
+
242
+ .ui-table {
243
+ width: 100%;
244
+ border-collapse: collapse;
245
+ font-size: 0.84rem;
246
+ text-align: left;
247
+ }
248
+
249
+ .ui-table-head {
250
+ background: var(--bg-panel-soft);
251
+ border-bottom: 1px solid var(--border-color);
252
+ }
253
+
254
+ .ui-table-header {
255
+ padding: 12px 16px;
256
+ font-weight: 700;
257
+ color: var(--text-muted);
258
+ text-transform: uppercase;
259
+ font-size: 0.7rem;
260
+ letter-spacing: 0.08em;
261
+ white-space: nowrap;
262
+ }
263
+
264
+ .ui-table-row {
265
+ border-bottom: 1px solid rgba(215, 210, 200, 0.8);
266
+ transition: background-color 0.18s;
267
+ }
268
+
269
+ .ui-table-row:hover {
270
+ background-color: rgba(13, 45, 82, 0.025);
271
+ }
272
+
273
+ .ui-table-cell {
274
+ padding: 12px 16px;
275
+ color: var(--text-main);
276
+ vertical-align: middle;
277
+ }
278
+
279
+ /* Tabs */
280
+ .ui-tabs-container {
281
+ display: flex;
282
+ flex-direction: column;
283
+ }
284
+
285
+ .ui-tabs-header {
286
+ display: flex;
287
+ border-bottom: 1px solid var(--border-color);
288
+ margin-bottom: 24px;
289
+ }
290
+
291
+ .ui-tab-button {
292
+ padding: 12px 18px;
293
+ background: none;
294
+ border: none;
295
+ border-bottom: 2px solid transparent;
296
+ font-size: 0.86rem;
297
+ font-weight: 700;
298
+ color: var(--text-muted);
299
+ cursor: pointer;
300
+ transition: all 0.18s;
301
+ font-family: inherit;
302
+ margin-bottom: -1px;
303
+ text-transform: uppercase;
304
+ letter-spacing: 0.05em;
305
+ }
306
+
307
+ .ui-tab-button:hover {
308
+ color: var(--color-brand-dark);
309
+ }
310
+
311
+ .ui-tab-button.active {
312
+ color: var(--color-brand-dark);
313
+ border-bottom-color: var(--color-brand-accent);
314
+ }
315
+
316
+ .ui-tabs-content {
317
+ flex: 1;
318
+ }
apps/web_comercial/tsconfig.json ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": [
5
+ "dom",
6
+ "dom.iterable",
7
+ "esnext"
8
+ ],
9
+ "allowJs": true,
10
+ "skipLibCheck": true,
11
+ "strict": true,
12
+ "noEmit": true,
13
+ "esModuleInterop": true,
14
+ "module": "esnext",
15
+ "moduleResolution": "bundler",
16
+ "resolveJsonModule": true,
17
+ "isolatedModules": true,
18
+ "jsx": "react-jsx",
19
+ "incremental": true,
20
+ "plugins": [
21
+ {
22
+ "name": "next"
23
+ }
24
+ ],
25
+ "paths": {
26
+ "@/*": [
27
+ "./src/*"
28
+ ]
29
+ }
30
+ },
31
+ "include": [
32
+ "next-env.d.ts",
33
+ "**/*.ts",
34
+ "**/*.tsx",
35
+ ".next/types/**/*.ts",
36
+ ".next/dev/types/**/*.ts",
37
+ "**/*.mts"
38
+ ],
39
+ "exclude": [
40
+ "node_modules"
41
+ ]
42
+ }
packages/simulation-engine/package.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "@simulador-consorcio/simulation-engine",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js"
9
+ },
10
+ "types": "./src/index.d.ts",
11
+ "scripts": {
12
+ "test": "node --test tests/*.test.js"
13
+ }
14
+ }
packages/simulation-engine/src/generated/rodobensFieldMatrix.v1.json ADDED
@@ -0,0 +1,838 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "artifact_version": "1.0",
3
+ "source_workbook": "SIMULADOR RODOBENS - BTG 5.xlsm",
4
+ "rows": [
5
+ {
6
+ "field_id": "partner_name",
7
+ "plan": "degrau",
8
+ "section": "header",
9
+ "source_sheet": null,
10
+ "source_cell": null,
11
+ "source_formula": "=IF('Degrau e Degrau Reduzido'!#REF!=\"\",\"\",'Degrau e Degrau Reduzido'!#REF!)",
12
+ "layout_sheet": "Simulador Degrau",
13
+ "layout_cell": "D6",
14
+ "status": "pending",
15
+ "current_value": "#REF!",
16
+ "notes": "Único #REF! confirmado do workbook; sem origem homologada."
17
+ },
18
+ {
19
+ "field_id": "proposal_valid_until",
20
+ "plan": "degrau",
21
+ "section": "header",
22
+ "source_sheet": "Simulador Degrau",
23
+ "source_cell": "D7",
24
+ "source_formula": "=TODAY()+3",
25
+ "layout_sheet": "Simulador Degrau",
26
+ "layout_cell": "D7",
27
+ "status": "presentation_only",
28
+ "current_value": "2026-02-13T00:00:00",
29
+ "notes": "Regra de apresentação via TODAY()+3."
30
+ },
31
+ {
32
+ "field_id": "plan_label",
33
+ "plan": "degrau",
34
+ "section": "header",
35
+ "source_sheet": "Degrau e Degrau Reduzido",
36
+ "source_cell": "J36",
37
+ "source_formula": "=\"Degrau \" &P8",
38
+ "layout_sheet": "Simulador Degrau",
39
+ "layout_cell": "C9",
40
+ "status": "extracted",
41
+ "current_value": "Degrau Integral",
42
+ "notes": "Nome comercial do plano derivado de P8."
43
+ },
44
+ {
45
+ "field_id": "numero_cotas",
46
+ "plan": "degrau",
47
+ "section": "input",
48
+ "source_sheet": "Degrau e Degrau Reduzido",
49
+ "source_cell": "M5",
50
+ "source_formula": null,
51
+ "layout_sheet": "Simulador Degrau",
52
+ "layout_cell": "I46",
53
+ "status": "extracted",
54
+ "current_value": 1,
55
+ "notes": "Fonte numérica em M5; layout final textual em L32/I46."
56
+ },
57
+ {
58
+ "field_id": "credito_contratado",
59
+ "plan": "degrau",
60
+ "section": "output",
61
+ "source_sheet": "Degrau e Degrau Reduzido",
62
+ "source_cell": "T3",
63
+ "source_formula": "=P4",
64
+ "layout_sheet": "Simulador Degrau",
65
+ "layout_cell": "D33",
66
+ "status": "extracted",
67
+ "current_value": 1000000,
68
+ "notes": "Layout multiplica T3 por M5."
69
+ },
70
+ {
71
+ "field_id": "lance_total",
72
+ "plan": "degrau",
73
+ "section": "output",
74
+ "source_sheet": "Degrau e Degrau Reduzido",
75
+ "source_cell": "C24",
76
+ "source_formula": "=IFERROR(S6*M5,\"\")",
77
+ "layout_sheet": "Simulador Degrau",
78
+ "layout_cell": "D34",
79
+ "status": "extracted",
80
+ "current_value": 650740.7407407408,
81
+ "notes": "Valor total consolidado do lance."
82
+ },
83
+ {
84
+ "field_id": "lance_embutido_valor",
85
+ "plan": "degrau",
86
+ "section": "output",
87
+ "source_sheet": "Degrau e Degrau Reduzido",
88
+ "source_cell": "D27",
89
+ "source_formula": "=T9*M5",
90
+ "layout_sheet": "Simulador Degrau",
91
+ "layout_cell": "D35",
92
+ "status": "extracted",
93
+ "current_value": 300000,
94
+ "notes": "Deriva de AD32 = P4 * P10."
95
+ },
96
+ {
97
+ "field_id": "recurso_proprio",
98
+ "plan": "degrau",
99
+ "section": "output",
100
+ "source_sheet": "Degrau e Degrau Reduzido",
101
+ "source_cell": "C28",
102
+ "source_formula": "=IFERROR((C24-D27),\"\")",
103
+ "layout_sheet": "Simulador Degrau",
104
+ "layout_cell": "D36",
105
+ "status": "extracted",
106
+ "current_value": 350740.7407407408,
107
+ "notes": "Valor residual após lance embutido."
108
+ },
109
+ {
110
+ "field_id": "primeira_parcela",
111
+ "plan": "degrau",
112
+ "section": "output",
113
+ "source_sheet": "Degrau e Degrau Reduzido",
114
+ "source_cell": "C26",
115
+ "source_formula": "=S8*M5",
116
+ "layout_sheet": "Simulador Degrau",
117
+ "layout_cell": "D37",
118
+ "status": "extracted",
119
+ "current_value": 6851.8518518519,
120
+ "notes": "Escolhe parcela integral ou reduzida conforme o plano."
121
+ },
122
+ {
123
+ "field_id": "credito_disponivel",
124
+ "plan": "degrau",
125
+ "section": "output",
126
+ "source_sheet": "Degrau e Degrau Reduzido",
127
+ "source_cell": "B42",
128
+ "source_formula": "=C33",
129
+ "layout_sheet": "Simulador Degrau",
130
+ "layout_cell": "D38",
131
+ "status": "extracted",
132
+ "current_value": 700000,
133
+ "notes": "Aponta para C33 na memória de cálculo."
134
+ },
135
+ {
136
+ "field_id": "credito_liquido",
137
+ "plan": "degrau",
138
+ "section": "output",
139
+ "source_sheet": "Degrau e Degrau Reduzido",
140
+ "source_cell": "C33",
141
+ "source_formula": "=T15*M5",
142
+ "layout_sheet": "Simulador Degrau",
143
+ "layout_cell": "D42",
144
+ "status": "review",
145
+ "current_value": 700000,
146
+ "notes": "Hoje coincide com crédito disponível; requer saneamento semântico."
147
+ },
148
+ {
149
+ "field_id": "furo",
150
+ "plan": "degrau",
151
+ "section": "input",
152
+ "source_sheet": "Degrau e Degrau Reduzido",
153
+ "source_cell": "M8",
154
+ "source_formula": null,
155
+ "layout_sheet": "Simulador Degrau",
156
+ "layout_cell": "D43",
157
+ "status": "extracted",
158
+ "current_value": 0,
159
+ "notes": "Quantidade de parcelas em atraso/furo."
160
+ },
161
+ {
162
+ "field_id": "mes_previsto_lance",
163
+ "plan": "degrau",
164
+ "section": "input",
165
+ "source_sheet": "Degrau e Degrau Reduzido",
166
+ "source_cell": "P17",
167
+ "source_formula": null,
168
+ "layout_sheet": "Simulador Degrau",
169
+ "layout_cell": "D44",
170
+ "status": "review",
171
+ "current_value": 12,
172
+ "notes": "Layout rotula como mês de contemplação, mas a origem funcional é mês previsto de lance."
173
+ },
174
+ {
175
+ "field_id": "seguro_prestamista",
176
+ "plan": "degrau",
177
+ "section": "output",
178
+ "source_sheet": "Degrau e Degrau Reduzido",
179
+ "source_cell": "AD46",
180
+ "source_formula": null,
181
+ "layout_sheet": "Simulador Degrau",
182
+ "layout_cell": "I42",
183
+ "status": "extracted_partial",
184
+ "current_value": 0,
185
+ "notes": "Fórmula de array; manter fixture própria."
186
+ },
187
+ {
188
+ "field_id": "fundo_reserva",
189
+ "plan": "degrau",
190
+ "section": "output",
191
+ "source_sheet": "Degrau e Degrau Reduzido",
192
+ "source_cell": "AD43",
193
+ "source_formula": null,
194
+ "layout_sheet": "Simulador Degrau",
195
+ "layout_cell": "I43",
196
+ "status": "extracted",
197
+ "current_value": 0,
198
+ "notes": "Atualmente zero."
199
+ },
200
+ {
201
+ "field_id": "taxa_administracao_mensal_exibida",
202
+ "plan": "degrau",
203
+ "section": "output",
204
+ "source_sheet": "Degrau e Degrau Reduzido",
205
+ "source_cell": "J42",
206
+ "source_formula": "=ROUND((P6/P5),6)*100 & \" % a.m.\"",
207
+ "layout_sheet": "Simulador Degrau",
208
+ "layout_cell": "I44",
209
+ "status": "review",
210
+ "current_value": "0,1111 % a.m.",
211
+ "notes": "Layout mostra taxa mensal em vez de taxa total."
212
+ },
213
+ {
214
+ "field_id": "taxa_adesao",
215
+ "plan": "degrau",
216
+ "section": "output",
217
+ "source_sheet": "Degrau e Degrau Reduzido",
218
+ "source_cell": "AD42",
219
+ "source_formula": null,
220
+ "layout_sheet": "Simulador Degrau",
221
+ "layout_cell": "I45",
222
+ "status": "extracted",
223
+ "current_value": 0,
224
+ "notes": "Atualmente zero."
225
+ },
226
+ {
227
+ "field_id": "diluicao_50_faixa_1",
228
+ "plan": "degrau",
229
+ "section": "output",
230
+ "source_sheet": "Degrau e Degrau Reduzido",
231
+ "source_cell": "S11",
232
+ "source_formula": "=\"de \" & P17+1& \" º até \"&P5*0.5&\"º \"",
233
+ "layout_sheet": "Simulador Degrau",
234
+ "layout_cell": "H34",
235
+ "status": "extracted",
236
+ "current_value": "de 13 º até 108º ",
237
+ "notes": "Faixa textual de diluição 50% - primeira metade."
238
+ },
239
+ {
240
+ "field_id": "diluicao_50_valor_1",
241
+ "plan": "degrau",
242
+ "section": "output",
243
+ "source_sheet": "Degrau e Degrau Reduzido",
244
+ "source_cell": "D29",
245
+ "source_formula": "=IFERROR(T11*M5,\"\")",
246
+ "layout_sheet": "Simulador Degrau",
247
+ "layout_cell": "I34",
248
+ "status": "extracted",
249
+ "current_value": 4511.0578204103,
250
+ "notes": "Valor monetário da primeira faixa de diluição 50%."
251
+ },
252
+ {
253
+ "field_id": "diluicao_50_faixa_2",
254
+ "plan": "degrau",
255
+ "section": "output",
256
+ "source_sheet": "Degrau e Degrau Reduzido",
257
+ "source_cell": "S12",
258
+ "source_formula": "=IFERROR(\"de \" & (P5*0.5)+1 &\"º até \" & ROUND((P5-AD36),\"00\")&\"º\",\" \")",
259
+ "layout_sheet": "Simulador Degrau",
260
+ "layout_cell": "H35",
261
+ "status": "extracted",
262
+ "current_value": "de 109º até 151º",
263
+ "notes": "Faixa textual de diluição 50% - segunda metade."
264
+ },
265
+ {
266
+ "field_id": "diluicao_50_valor_2",
267
+ "plan": "degrau",
268
+ "section": "output",
269
+ "source_sheet": "Degrau e Degrau Reduzido",
270
+ "source_cell": "D30",
271
+ "source_formula": "=IFERROR(T12*M5,\"\")",
272
+ "layout_sheet": "Simulador Degrau",
273
+ "layout_cell": "I35",
274
+ "status": "extracted",
275
+ "current_value": 2288.8355981881,
276
+ "notes": "Valor monetário da segunda faixa de diluição 50%."
277
+ },
278
+ {
279
+ "field_id": "diluicao_100_faixa_1",
280
+ "plan": "degrau",
281
+ "section": "output",
282
+ "source_sheet": "Degrau e Degrau Reduzido",
283
+ "source_cell": "S13",
284
+ "source_formula": "=\"de \" & P17+1& \" º até \"&P5*0.5&\"º \"",
285
+ "layout_sheet": "Simulador Degrau",
286
+ "layout_cell": "H37",
287
+ "status": "extracted",
288
+ "current_value": "de 13 º até 108º ",
289
+ "notes": "Faixa textual de diluição 100% - primeira metade."
290
+ },
291
+ {
292
+ "field_id": "diluicao_100_valor_1",
293
+ "plan": "degrau",
294
+ "section": "output",
295
+ "source_sheet": "Degrau e Degrau Reduzido",
296
+ "source_cell": "D31",
297
+ "source_formula": "=IFERROR(T13*M5,\"\")",
298
+ "layout_sheet": "Simulador Degrau",
299
+ "layout_cell": "I37",
300
+ "status": "extracted",
301
+ "current_value": 3661.9462599855,
302
+ "notes": "Valor monetário da primeira faixa de diluição 100%."
303
+ },
304
+ {
305
+ "field_id": "diluicao_100_faixa_2",
306
+ "plan": "degrau",
307
+ "section": "output",
308
+ "source_sheet": "Degrau e Degrau Reduzido",
309
+ "source_cell": "S14",
310
+ "source_formula": "= \"de \" &(P5/2)+1 & \"º até \" &P5-M8 &\"º \"",
311
+ "layout_sheet": "Simulador Degrau",
312
+ "layout_cell": "H38",
313
+ "status": "extracted",
314
+ "current_value": "de 109º até 216º ",
315
+ "notes": "Faixa textual de diluição 100% - segunda metade."
316
+ },
317
+ {
318
+ "field_id": "diluicao_100_valor_2",
319
+ "plan": "degrau",
320
+ "section": "output",
321
+ "source_sheet": "Degrau e Degrau Reduzido",
322
+ "source_cell": "D32",
323
+ "source_formula": "=IFERROR(T14*M5,\"\")",
324
+ "layout_sheet": "Simulador Degrau",
325
+ "layout_cell": "I38",
326
+ "status": "extracted",
327
+ "current_value": 1439.7240377633,
328
+ "notes": "Valor monetário da segunda faixa de diluição 100%."
329
+ },
330
+ {
331
+ "field_id": "lance_observacao",
332
+ "plan": "degrau",
333
+ "section": "presentation",
334
+ "source_sheet": "Degrau e Degrau Reduzido",
335
+ "source_cell": "L35",
336
+ "source_formula": "=IF(AND(M6=\"Livre\",P5>48),\"Opção de utilizar o lance para quitar até 03 parcelas seguintes à contemplação\",\"Opção de utilizar o lance para quitar até 01 parcela seguinte à contemplação\")",
337
+ "layout_sheet": "Simulador Degrau",
338
+ "layout_cell": "B45",
339
+ "status": "extracted",
340
+ "current_value": "Opção de utilizar o lance para quitar até 03 parcelas seguintes à contemplação",
341
+ "notes": "Texto comercial dependente de modalidade de lance."
342
+ },
343
+ {
344
+ "field_id": "partner_name",
345
+ "plan": "linear",
346
+ "section": "header",
347
+ "source_sheet": "Linear e Linear Reduzido",
348
+ "source_cell": "AF1",
349
+ "source_formula": null,
350
+ "layout_sheet": "Simulador Linear",
351
+ "layout_cell": "D6",
352
+ "status": "review",
353
+ "current_value": "Paula",
354
+ "notes": "Aba chama o campo de Nome Cliente; layout imprime Parceiro."
355
+ },
356
+ {
357
+ "field_id": "proposal_valid_until",
358
+ "plan": "linear",
359
+ "section": "header",
360
+ "source_sheet": "Simulador Linear",
361
+ "source_cell": "D7",
362
+ "source_formula": "=TODAY()+3",
363
+ "layout_sheet": "Simulador Linear",
364
+ "layout_cell": "D7",
365
+ "status": "presentation_only",
366
+ "current_value": "2026-02-13T00:00:00",
367
+ "notes": "Regra de apresentação via TODAY()+3."
368
+ },
369
+ {
370
+ "field_id": "plan_label",
371
+ "plan": "linear",
372
+ "section": "header",
373
+ "source_sheet": "Linear e Linear Reduzido",
374
+ "source_cell": "K8",
375
+ "source_formula": null,
376
+ "layout_sheet": "Simulador Linear",
377
+ "layout_cell": "C9",
378
+ "status": "extracted",
379
+ "current_value": "Linear Reduzido 70%",
380
+ "notes": "Nome do plano atual."
381
+ },
382
+ {
383
+ "field_id": "grupo",
384
+ "plan": "linear",
385
+ "section": "input",
386
+ "source_sheet": "Linear e Linear Reduzido",
387
+ "source_cell": "G4",
388
+ "source_formula": null,
389
+ "layout_sheet": "Simulador Linear",
390
+ "layout_cell": "D36",
391
+ "status": "extracted",
392
+ "current_value": 2234,
393
+ "notes": "Grupo comercial."
394
+ },
395
+ {
396
+ "field_id": "numero_cotas",
397
+ "plan": "linear",
398
+ "section": "input",
399
+ "source_sheet": "Linear e Linear Reduzido",
400
+ "source_cell": "G5",
401
+ "source_formula": null,
402
+ "layout_sheet": null,
403
+ "layout_cell": null,
404
+ "status": "extracted",
405
+ "current_value": 5,
406
+ "notes": "Quantidade numérica; layout só expõe texto em AE13."
407
+ },
408
+ {
409
+ "field_id": "modalidade_lance",
410
+ "plan": "linear",
411
+ "section": "input",
412
+ "source_sheet": "Linear e Linear Reduzido",
413
+ "source_cell": "G6",
414
+ "source_formula": null,
415
+ "layout_sheet": null,
416
+ "layout_cell": null,
417
+ "status": "extracted",
418
+ "current_value": "Fixo",
419
+ "notes": "Livre ou fixo."
420
+ },
421
+ {
422
+ "field_id": "tipo_produto",
423
+ "plan": "linear",
424
+ "section": "input",
425
+ "source_sheet": "Linear e Linear Reduzido",
426
+ "source_cell": "G7",
427
+ "source_formula": null,
428
+ "layout_sheet": null,
429
+ "layout_cell": null,
430
+ "status": "extracted",
431
+ "current_value": "Imóvel",
432
+ "notes": "Produto do cenário."
433
+ },
434
+ {
435
+ "field_id": "furo",
436
+ "plan": "linear",
437
+ "section": "input",
438
+ "source_sheet": "Linear e Linear Reduzido",
439
+ "source_cell": "AN19",
440
+ "source_formula": "=G8",
441
+ "layout_sheet": "Simulador Linear",
442
+ "layout_cell": "D35",
443
+ "status": "extracted",
444
+ "current_value": 0,
445
+ "notes": "AN19 espelha G8."
446
+ },
447
+ {
448
+ "field_id": "credito_base",
449
+ "plan": "linear",
450
+ "section": "input",
451
+ "source_sheet": "Linear e Linear Reduzido",
452
+ "source_cell": "K4",
453
+ "source_formula": null,
454
+ "layout_sheet": null,
455
+ "layout_cell": null,
456
+ "status": "extracted",
457
+ "current_value": 200000,
458
+ "notes": "Base do cálculo."
459
+ },
460
+ {
461
+ "field_id": "prazo_cliente",
462
+ "plan": "linear",
463
+ "section": "input",
464
+ "source_sheet": "Linear e Linear Reduzido",
465
+ "source_cell": "K5",
466
+ "source_formula": null,
467
+ "layout_sheet": null,
468
+ "layout_cell": null,
469
+ "status": "extracted",
470
+ "current_value": 180,
471
+ "notes": "Prazo em meses."
472
+ },
473
+ {
474
+ "field_id": "taxa_administracao_total",
475
+ "plan": "linear",
476
+ "section": "input",
477
+ "source_sheet": "Linear e Linear Reduzido",
478
+ "source_cell": "K6",
479
+ "source_formula": null,
480
+ "layout_sheet": null,
481
+ "layout_cell": null,
482
+ "status": "extracted",
483
+ "current_value": 0.21,
484
+ "notes": "Taxa contratual total."
485
+ },
486
+ {
487
+ "field_id": "taxa_administracao_mensal_exibida",
488
+ "plan": "linear",
489
+ "section": "output",
490
+ "source_sheet": "Linear e Linear Reduzido",
491
+ "source_cell": "M75",
492
+ "source_formula": "=ROUND(((K6/K5))*100,5)&\" % a.m.\"",
493
+ "layout_sheet": "Simulador Linear",
494
+ "layout_cell": "I36",
495
+ "status": "review",
496
+ "current_value": "0.11667 % a.m.",
497
+ "notes": "Layout mostra taxa mensal e não a taxa total."
498
+ },
499
+ {
500
+ "field_id": "seguro_prestamista",
501
+ "plan": "linear",
502
+ "section": "output",
503
+ "source_sheet": "Linear e Linear Reduzido",
504
+ "source_cell": "AF30",
505
+ "source_formula": "=IF(K7=\"Não\",0,_xlfn.XLOOKUP(G7,Tabela2[Produto],Tabela2[Taxa],0,0))",
506
+ "layout_sheet": "Simulador Linear",
507
+ "layout_cell": "I35",
508
+ "status": "extracted_partial",
509
+ "current_value": 0.0003245,
510
+ "notes": "Lookup por produto em Tabela2."
511
+ },
512
+ {
513
+ "field_id": "tipo_lance",
514
+ "plan": "linear",
515
+ "section": "input",
516
+ "source_sheet": "Linear e Linear Reduzido",
517
+ "source_cell": "K9",
518
+ "source_formula": null,
519
+ "layout_sheet": null,
520
+ "layout_cell": null,
521
+ "status": "extracted",
522
+ "current_value": "Lance Parcela Integral",
523
+ "notes": "Tipo de lance do cenário."
524
+ },
525
+ {
526
+ "field_id": "embutido_percentual",
527
+ "plan": "linear",
528
+ "section": "input",
529
+ "source_sheet": "Linear e Linear Reduzido",
530
+ "source_cell": "K10",
531
+ "source_formula": null,
532
+ "layout_sheet": null,
533
+ "layout_cell": null,
534
+ "status": "extracted",
535
+ "current_value": 0,
536
+ "notes": "Percentual de embutido."
537
+ },
538
+ {
539
+ "field_id": "parcelas_lance",
540
+ "plan": "linear",
541
+ "section": "input",
542
+ "source_sheet": "Linear e Linear Reduzido",
543
+ "source_cell": "K13",
544
+ "source_formula": null,
545
+ "layout_sheet": null,
546
+ "layout_cell": null,
547
+ "status": "extracted",
548
+ "current_value": 54,
549
+ "notes": "Quantidade de parcelas do lance."
550
+ },
551
+ {
552
+ "field_id": "mes_previsto_lance",
553
+ "plan": "linear",
554
+ "section": "input",
555
+ "source_sheet": "Linear e Linear Reduzido",
556
+ "source_cell": "K14",
557
+ "source_formula": null,
558
+ "layout_sheet": "Simulador Linear",
559
+ "layout_cell": "D37",
560
+ "status": "review",
561
+ "current_value": 36,
562
+ "notes": "Layout trata como mês de contemplação."
563
+ },
564
+ {
565
+ "field_id": "lance_total",
566
+ "plan": "linear",
567
+ "section": "output",
568
+ "source_sheet": "Linear e Linear Reduzido",
569
+ "source_cell": "AN20",
570
+ "source_formula": "=IFERROR(O7*G5,\"Verificar Tipo de Lance\")",
571
+ "layout_sheet": null,
572
+ "layout_cell": null,
573
+ "status": "extracted",
574
+ "current_value": 384202.83,
575
+ "notes": "AN20 consolida o valor total por número de cotas."
576
+ },
577
+ {
578
+ "field_id": "credito_disponivel",
579
+ "plan": "linear",
580
+ "section": "output",
581
+ "source_sheet": "Linear e Linear Reduzido",
582
+ "source_cell": "AN27",
583
+ "source_formula": "=O14*G5",
584
+ "layout_sheet": null,
585
+ "layout_cell": null,
586
+ "status": "extracted",
587
+ "current_value": 1000000,
588
+ "notes": "AN27 consolida O14 por número de cotas."
589
+ },
590
+ {
591
+ "field_id": "diluicao_50_texto",
592
+ "plan": "linear",
593
+ "section": "output",
594
+ "source_sheet": "Linear e Linear Reduzido",
595
+ "source_cell": "AE12",
596
+ "source_formula": "=IF(K8=\"Linear Integral\",\"\",\"Plano de Parcela Reduzida até a contemplação\")",
597
+ "layout_sheet": "Simulador Linear",
598
+ "layout_cell": "B38",
599
+ "status": "extracted_partial",
600
+ "current_value": "Plano de Parcela Reduzida até a contemplação",
601
+ "notes": "Layout não expõe os valores monetários de diluição."
602
+ },
603
+ {
604
+ "field_id": "diluicao_50_valor",
605
+ "plan": "linear",
606
+ "section": "output",
607
+ "source_sheet": "Linear e Linear Reduzido",
608
+ "source_cell": "O12",
609
+ "source_formula": "=IF(IFERROR(G18-AF18,\"Verificar Tipo de Lance\")<0,\"\",IFERROR(G18-AF18,\"Verificar Tipo de Lance\"))",
610
+ "layout_sheet": null,
611
+ "layout_cell": null,
612
+ "status": "extracted",
613
+ "current_value": 1218.6975213675,
614
+ "notes": "Valor monetário da diluição 50%."
615
+ },
616
+ {
617
+ "field_id": "diluicao_100_faixa",
618
+ "plan": "linear",
619
+ "section": "output",
620
+ "source_sheet": "Linear e Linear Reduzido",
621
+ "source_cell": "N13",
622
+ "source_formula": "=\"de \"&K14+1&\"º\" & \" a \" &(K5)-G8& \"º\"",
623
+ "layout_sheet": null,
624
+ "layout_cell": null,
625
+ "status": "extracted",
626
+ "current_value": "de 37º a 180º",
627
+ "notes": "Faixa textual da diluição 100%."
628
+ },
629
+ {
630
+ "field_id": "diluicao_100_valor",
631
+ "plan": "linear",
632
+ "section": "output",
633
+ "source_sheet": "Linear e Linear Reduzido",
634
+ "source_cell": "O13",
635
+ "source_formula": "=IFERROR(G18-AF19,\"Verificar Tipo de Lance\")",
636
+ "layout_sheet": null,
637
+ "layout_cell": null,
638
+ "status": "extracted",
639
+ "current_value": 990.1917361111,
640
+ "notes": "Valor monetário da diluição 100%."
641
+ },
642
+ {
643
+ "field_id": "partner_name",
644
+ "plan": "pontual",
645
+ "section": "header",
646
+ "source_sheet": "Pontual",
647
+ "source_cell": "AB6",
648
+ "source_formula": null,
649
+ "layout_sheet": "Simulador Pontual",
650
+ "layout_cell": "D6",
651
+ "status": "extracted",
652
+ "current_value": "Paula",
653
+ "notes": "Campo simples e estável."
654
+ },
655
+ {
656
+ "field_id": "taxa_adesao",
657
+ "plan": "pontual",
658
+ "section": "output",
659
+ "source_sheet": "Pontual",
660
+ "source_cell": "AB1",
661
+ "source_formula": null,
662
+ "layout_sheet": "Simulador Pontual",
663
+ "layout_cell": "D44",
664
+ "status": "extracted",
665
+ "current_value": 0,
666
+ "notes": "Atualmente zero."
667
+ },
668
+ {
669
+ "field_id": "fundo_reserva",
670
+ "plan": "pontual",
671
+ "section": "output",
672
+ "source_sheet": "Pontual",
673
+ "source_cell": "AB5",
674
+ "source_formula": null,
675
+ "layout_sheet": "Simulador Pontual",
676
+ "layout_cell": "I44",
677
+ "status": "extracted",
678
+ "current_value": 0,
679
+ "notes": "Atualmente zero."
680
+ },
681
+ {
682
+ "field_id": "furo",
683
+ "plan": "pontual",
684
+ "section": "input",
685
+ "source_sheet": "Pontual",
686
+ "source_cell": "O4",
687
+ "source_formula": null,
688
+ "layout_sheet": "Simulador Pontual",
689
+ "layout_cell": "D43",
690
+ "status": "extracted",
691
+ "current_value": 0,
692
+ "notes": "Quantidade de parcelas em atraso/furo."
693
+ },
694
+ {
695
+ "field_id": "option_title",
696
+ "plan": "pontual",
697
+ "section": "output",
698
+ "source_sheet": "Pontual",
699
+ "source_cell": "AA22",
700
+ "source_formula": "=IF($AB$3=\"Automóvel\",G31,D31)",
701
+ "layout_sheet": "Simulador Pontual",
702
+ "layout_cell": "C32",
703
+ "status": "domain_conflict",
704
+ "current_value": "Contemplação por Sorteio (Até 12 Meses)",
705
+ "notes": "Depende de AB3 e alterna entre branches Auto/Imóvel."
706
+ },
707
+ {
708
+ "field_id": "produto_base_controle",
709
+ "plan": "pontual",
710
+ "section": "input",
711
+ "source_sheet": "Pontual",
712
+ "source_cell": "AB3",
713
+ "source_formula": null,
714
+ "layout_sheet": null,
715
+ "layout_cell": null,
716
+ "status": "domain_conflict",
717
+ "current_value": "Automóvel",
718
+ "notes": "Workbook atual gravado como Automóvel."
719
+ },
720
+ {
721
+ "field_id": "tipo_produto_operacional",
722
+ "plan": "pontual",
723
+ "section": "input",
724
+ "source_sheet": "Pontual",
725
+ "source_cell": "O6",
726
+ "source_formula": null,
727
+ "layout_sheet": null,
728
+ "layout_cell": null,
729
+ "status": "domain_conflict",
730
+ "current_value": "Automóvel",
731
+ "notes": "Workbook atual gravado como Automóvel."
732
+ },
733
+ {
734
+ "field_id": "produto_chave_auto",
735
+ "plan": "pontual",
736
+ "section": "control",
737
+ "source_sheet": "Pontual",
738
+ "source_cell": "I47",
739
+ "source_formula": null,
740
+ "layout_sheet": null,
741
+ "layout_cell": null,
742
+ "status": "domain_conflict",
743
+ "current_value": "Automóvel",
744
+ "notes": "Chave de comparação legada para Auto."
745
+ },
746
+ {
747
+ "field_id": "produto_chave_imovel",
748
+ "plan": "pontual",
749
+ "section": "control",
750
+ "source_sheet": "Pontual",
751
+ "source_cell": "I48",
752
+ "source_formula": null,
753
+ "layout_sheet": null,
754
+ "layout_cell": null,
755
+ "status": "domain_conflict",
756
+ "current_value": "Imóvel",
757
+ "notes": "Chave de comparação legada para Imóvel."
758
+ },
759
+ {
760
+ "field_id": "numero_cotas",
761
+ "plan": "pontual",
762
+ "section": "input",
763
+ "source_sheet": "Pontual",
764
+ "source_cell": "O5",
765
+ "source_formula": null,
766
+ "layout_sheet": null,
767
+ "layout_cell": null,
768
+ "status": "extracted",
769
+ "current_value": 1,
770
+ "notes": "Quantidade numérica; texto final em P8."
771
+ },
772
+ {
773
+ "field_id": "numero_cotas_texto",
774
+ "plan": "pontual",
775
+ "section": "presentation",
776
+ "source_sheet": "Pontual",
777
+ "source_cell": "P8",
778
+ "source_formula": "=IF(O5=1,O5&\" cota\",O5&\" cotas\")",
779
+ "layout_sheet": null,
780
+ "layout_cell": null,
781
+ "status": "extracted",
782
+ "current_value": "1 cota",
783
+ "notes": "Texto comercial da quantidade de cotas."
784
+ },
785
+ {
786
+ "field_id": "valor_parcela_com_seguro",
787
+ "plan": "pontual",
788
+ "section": "output",
789
+ "source_sheet": "Pontual",
790
+ "source_cell": "L7",
791
+ "source_formula": "=O47/L4",
792
+ "layout_sheet": null,
793
+ "layout_cell": null,
794
+ "status": "extracted",
795
+ "current_value": 1323.2462667038,
796
+ "notes": "Valor da parcela com seguro."
797
+ },
798
+ {
799
+ "field_id": "valor_parcela_sem_seguro",
800
+ "plan": "pontual",
801
+ "section": "output",
802
+ "source_sheet": "Pontual",
803
+ "source_cell": "L8",
804
+ "source_formula": "=O44/L4",
805
+ "layout_sheet": null,
806
+ "layout_cell": null,
807
+ "status": "extracted",
808
+ "current_value": 1257.6210833333,
809
+ "notes": "Valor da parcela sem seguro."
810
+ },
811
+ {
812
+ "field_id": "credito_mais_taxas",
813
+ "plan": "pontual",
814
+ "section": "output",
815
+ "source_sheet": "Pontual",
816
+ "source_cell": "O44",
817
+ "source_formula": "=L3+(L3*L6)",
818
+ "layout_sheet": null,
819
+ "layout_cell": null,
820
+ "status": "extracted",
821
+ "current_value": 75457.265,
822
+ "notes": "Base de cálculo do saldo devedor."
823
+ },
824
+ {
825
+ "field_id": "saldo_devedor_total",
826
+ "plan": "pontual",
827
+ "section": "output",
828
+ "source_sheet": "Pontual",
829
+ "source_cell": "O47",
830
+ "source_formula": "=O44+O46",
831
+ "layout_sheet": null,
832
+ "layout_cell": null,
833
+ "status": "extracted",
834
+ "current_value": 79394.77600223,
835
+ "notes": "O44 + O46."
836
+ }
837
+ ]
838
+ }
packages/simulation-engine/src/index.d.ts ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface ParametrosPlano {
2
+ id_plano: string;
3
+ nome_plano: string;
4
+ prazo_meses: number;
5
+ taxa_administracao_total: number;
6
+ fundo_reserva_mensal: number;
7
+ seguro_prestamista: number;
8
+ }
9
+
10
+ export interface SimulacaoInput {
11
+ credito_desejado: number;
12
+ parametros: ParametrosPlano;
13
+ tem_lance: boolean;
14
+ lance_percentual?: number;
15
+ }
16
+
17
+ export interface ResultadoParcela {
18
+ fundo_comum: number;
19
+ taxa_administracao: number;
20
+ fundo_reserva: number;
21
+ seguro: number;
22
+ total: number;
23
+ }
24
+
25
+ export interface ResultadoSimulacao {
26
+ credito: number;
27
+ prazo: number;
28
+ parcela_integral: ResultadoParcela;
29
+ parcela_reduzida?: ResultadoParcela;
30
+ valor_lance?: number;
31
+ prazo_apos_lance?: number;
32
+ }
33
+
34
+ export interface RodobensParityInput {
35
+ plan: "degrau" | "linear" | "pontual";
36
+ partner_name?: string | null;
37
+ grupo?: number | string | null;
38
+ numero_cotas?: number;
39
+ modalidade_lance?: string;
40
+ tipo_produto?: string;
41
+ produto_base_controle?: string;
42
+ furo?: number;
43
+ credito: number;
44
+ prazo_cliente: number;
45
+ taxa_administrativa_total: number;
46
+ seguro_prestamista_habilitado?: string | boolean;
47
+ plano?: string;
48
+ tipo_lance?: string;
49
+ embutido_percentual?: number;
50
+ parcelas_lance?: number;
51
+ mes_previsto_lance?: number;
52
+ }
53
+
54
+ export interface ResultadoParidadeRodobens {
55
+ [fieldId: string]: string | number | null;
56
+ }
57
+
58
+ export interface TransacaoPortabilidade {
59
+ valor: number;
60
+ contemplada: boolean;
61
+ }
62
+
63
+ export interface SimulacaoSecundarioInput {
64
+ fluxos_mensais: number[];
65
+ transacoes_portabilidade?: TransacaoPortabilidade[];
66
+ portabilidade_percentual_contemplada?: number;
67
+ portabilidade_percentual_nao_contemplada?: number;
68
+ volume_contratado?: number;
69
+ fee_percentual?: number;
70
+ percentual_cartas_parcela_reduzida?: number;
71
+ meses_pos_ultima_contemplacao?: number;
72
+ sem_correcao_antes_contemplacao?: boolean;
73
+ }
74
+
75
+ export interface RegrasSecundarioAplicadas {
76
+ portabilidade_contemplada: number;
77
+ portabilidade_nao_contemplada: number;
78
+ sem_correcao_antes_contemplacao: boolean;
79
+ fee_sobre_volume_reduzido_70: boolean;
80
+ }
81
+
82
+ export interface ResultadoSimulacaoSecundario {
83
+ fluxo_ajustado: number[];
84
+ custo_portabilidade: number;
85
+ custo_fee_parcela_reduzida: number;
86
+ custo_total_deducoes: number;
87
+ caixa_maximo_negativo: number;
88
+ payback_meses: number | null;
89
+ tir_mensal: number | null;
90
+ tir_anual: number | null;
91
+ regras_aplicadas: RegrasSecundarioAplicadas;
92
+ }
93
+
94
+ export interface ScpFlowPatternSummary {
95
+ id: string;
96
+ nome: string;
97
+ descricao: string;
98
+ pontos: number;
99
+ inicio?: string;
100
+ fim?: string;
101
+ }
102
+
103
+ export interface ScpClientDefault {
104
+ client_id: string;
105
+ client_name: string;
106
+ default_pattern_id: string;
107
+ status: string;
108
+ }
109
+
110
+ export interface ScpFlowPoint {
111
+ periodo: string;
112
+ fluxo: number;
113
+ }
114
+
115
+ export interface MontarFluxoClienteScpInput {
116
+ client_id: string;
117
+ pattern_id?: string;
118
+ escala_fluxo?: number;
119
+ deslocamento_meses?: number;
120
+ novo_client_id?: string;
121
+ novo_client_name?: string;
122
+ }
123
+
124
+ export interface MontarFluxoClienteScpOutput {
125
+ client_id: string;
126
+ client_name: string;
127
+ source_client_id: string;
128
+ pattern_id: string;
129
+ pattern_name: string;
130
+ escala_fluxo: number;
131
+ deslocamento_meses: number;
132
+ pontos: ScpFlowPoint[];
133
+ fluxos_mensais: number[];
134
+ }
135
+
136
+ export interface SimularSecundarioComPerfilScpInput
137
+ extends MontarFluxoClienteScpInput,
138
+ Omit<SimulacaoSecundarioInput, "fluxos_mensais"> {}
139
+
140
+ export interface ResultadoSimulacaoSecundarioComPerfil
141
+ extends ResultadoSimulacaoSecundario {
142
+ fluxo_cliente: MontarFluxoClienteScpOutput;
143
+ }
144
+
145
+ export interface SimularConversaoScpConsorcioInput
146
+ extends SimularSecundarioComPerfilScpInput {
147
+ valor_imovel_base?: number;
148
+ desconto_imovel_percentual?: number;
149
+ percentual_entrada_imovel?: number;
150
+ agio_consorcio_percentual?: number;
151
+ taxa_custos_transacao_percentual?: number;
152
+ }
153
+
154
+ export interface ResultadoModoConversaoScpConsorcio {
155
+ natureza_cenario: string;
156
+ observacao: string;
157
+ valor_imovel_base: number;
158
+ desconto_imovel_percentual: number;
159
+ percentual_entrada_imovel: number;
160
+ agio_consorcio_percentual: number;
161
+ taxa_custos_transacao_percentual: number;
162
+ valor_imovel_com_desconto: number;
163
+ valor_entrada_imovel: number;
164
+ valor_credito_consorcio: number;
165
+ valor_custos_transacao: number;
166
+ investimento_total_conversao: number;
167
+ valor_saida_consorcio_secundario: number;
168
+ lucro_projetado_conversao: number;
169
+ roi_projetado_conversao: number | null;
170
+ }
171
+
172
+ export interface ResultadoSimulacaoConversaoScpConsorcio
173
+ extends ResultadoSimulacaoSecundarioComPerfil {
174
+ modo_conversao: ResultadoModoConversaoScpConsorcio;
175
+ }
176
+
177
+ export declare function calcularSimulacao(input: SimulacaoInput): ResultadoSimulacao;
178
+ export declare function calcularParidadeRodobens(
179
+ input: RodobensParityInput
180
+ ): ResultadoParidadeRodobens;
181
+ export declare function calcularIndicadoresSecundario(
182
+ input: SimulacaoSecundarioInput
183
+ ): ResultadoSimulacaoSecundario;
184
+ export declare function listarPerfisFluxoScpDefault(): {
185
+ clients: ScpClientDefault[];
186
+ patterns: ScpFlowPatternSummary[];
187
+ };
188
+ export declare function montarFluxoClienteScp(
189
+ input: MontarFluxoClienteScpInput
190
+ ): MontarFluxoClienteScpOutput;
191
+ export declare function simularSecundarioComPerfilScp(
192
+ input: SimularSecundarioComPerfilScpInput
193
+ ): ResultadoSimulacaoSecundarioComPerfil;
194
+ export declare function simularModoConversaoScpConsorcio(
195
+ input: SimularConversaoScpConsorcioInput
196
+ ): ResultadoSimulacaoConversaoScpConsorcio;
packages/simulation-engine/src/index.js ADDED
@@ -0,0 +1,823 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { SCP_CLIENTS_DEFAULT, SCP_FLOW_PATTERNS_DEFAULT } from "./scpDefaults.js";
2
+ import rodobensFieldMatrixBundle from "./generated/rodobensFieldMatrix.v1.json" with { type: "json" };
3
+
4
+ function ensurePositiveNumber(name, value) {
5
+ if (typeof value !== "number" || Number.isNaN(value) || value <= 0) {
6
+ throw new Error(`${name} deve ser um numero maior que zero`);
7
+ }
8
+ }
9
+
10
+ function ensureNonNegativeNumber(name, value) {
11
+ if (typeof value !== "number" || Number.isNaN(value) || value < 0) {
12
+ throw new Error(`${name} deve ser um numero nao negativo`);
13
+ }
14
+ }
15
+
16
+ function ensurePercent(name, value) {
17
+ if (typeof value !== "number" || Number.isNaN(value) || value < 0 || value > 1) {
18
+ throw new Error(`${name} deve estar entre 0 e 1`);
19
+ }
20
+ }
21
+
22
+ function ensureNumberArray(name, value) {
23
+ if (!Array.isArray(value) || value.length === 0) {
24
+ throw new Error(`${name} deve ser uma lista com ao menos um valor`);
25
+ }
26
+ for (const item of value) {
27
+ if (typeof item !== "number" || Number.isNaN(item)) {
28
+ throw new Error(`${name} deve conter apenas numeros`);
29
+ }
30
+ }
31
+ }
32
+
33
+ function ensureInteger(name, value) {
34
+ if (!Number.isInteger(value)) {
35
+ throw new Error(`${name} deve ser inteiro`);
36
+ }
37
+ }
38
+
39
+ function normalizeLookupKey(value) {
40
+ return String(value || "")
41
+ .normalize("NFD")
42
+ .replace(/[\u0300-\u036f]/g, "")
43
+ .trim()
44
+ .toLowerCase();
45
+ }
46
+
47
+ function formatNumberWithComma(value, decimals) {
48
+ return Number(value).toFixed(decimals).replace(".", ",");
49
+ }
50
+
51
+ function roundTo(value, decimals) {
52
+ const factor = 10 ** decimals;
53
+ return Math.round(value * factor) / factor;
54
+ }
55
+
56
+ const RODOBENS_FIELD_MATRIX_ROWS = Array.isArray(rodobensFieldMatrixBundle?.rows)
57
+ ? rodobensFieldMatrixBundle.rows
58
+ : [];
59
+
60
+ const RODOBENS_FIELD_MATRIX_BY_PLAN = RODOBENS_FIELD_MATRIX_ROWS.reduce((acc, row) => {
61
+ const current = acc.get(row.plan) || [];
62
+ current.push(row);
63
+ acc.set(row.plan, current);
64
+ return acc;
65
+ }, new Map());
66
+
67
+ const RODOBENS_PARITY_FIELD_ALIASES = {
68
+ mes_previsto_lance: "mes_de_contemplacao",
69
+ taxa_administracao_mensal_exibida: "taxa_administracao_exibida",
70
+ diluicao_50_texto: "texto_diluicao_50",
71
+ };
72
+
73
+ function addMonths(isoDate, deltaMonths) {
74
+ const [year, month, day] = isoDate.split("-").map(Number);
75
+ const base = new Date(Date.UTC(year, month - 1, day || 1));
76
+ base.setUTCMonth(base.getUTCMonth() + deltaMonths);
77
+ const y = base.getUTCFullYear();
78
+ const m = String(base.getUTCMonth() + 1).padStart(2, "0");
79
+ const d = String(base.getUTCDate()).padStart(2, "0");
80
+ return `${y}-${m}-${d}`;
81
+ }
82
+
83
+ function validateInput(input) {
84
+ if (!input || typeof input !== "object") {
85
+ throw new Error("input invalido");
86
+ }
87
+
88
+ ensurePositiveNumber("credito_desejado", input.credito_desejado);
89
+
90
+ if (!input.parametros || typeof input.parametros !== "object") {
91
+ throw new Error("parametros invalidos");
92
+ }
93
+
94
+ const p = input.parametros;
95
+
96
+ ensurePositiveNumber("prazo_meses", p.prazo_meses);
97
+ ensureNonNegativeNumber("taxa_administracao_total", p.taxa_administracao_total);
98
+ ensureNonNegativeNumber("fundo_reserva_mensal", p.fundo_reserva_mensal);
99
+ ensureNonNegativeNumber("seguro_prestamista", p.seguro_prestamista);
100
+
101
+ if (input.tem_lance && input.lance_percentual !== undefined) {
102
+ ensureNonNegativeNumber("lance_percentual", input.lance_percentual);
103
+ }
104
+ }
105
+
106
+ export function listarPerfisFluxoScpDefault() {
107
+ return {
108
+ clients: SCP_CLIENTS_DEFAULT.map((client) => ({ ...client })),
109
+ patterns: SCP_FLOW_PATTERNS_DEFAULT.map((pattern) => ({
110
+ id: pattern.id,
111
+ nome: pattern.nome,
112
+ descricao: pattern.descricao,
113
+ pontos: pattern.pontos.length,
114
+ inicio: pattern.pontos[0]?.periodo,
115
+ fim: pattern.pontos[pattern.pontos.length - 1]?.periodo,
116
+ })),
117
+ };
118
+ }
119
+
120
+ function findPattern(patternId) {
121
+ return SCP_FLOW_PATTERNS_DEFAULT.find((pattern) => pattern.id === patternId);
122
+ }
123
+
124
+ function findClient(clientId) {
125
+ return SCP_CLIENTS_DEFAULT.find((client) => client.client_id === clientId);
126
+ }
127
+
128
+ function validateScpFlowInput(input) {
129
+ if (!input || typeof input !== "object") {
130
+ throw new Error("input de fluxo scp invalido");
131
+ }
132
+ if (typeof input.client_id !== "string" || !input.client_id.trim()) {
133
+ throw new Error("client_id obrigatorio");
134
+ }
135
+ if (input.pattern_id !== undefined && typeof input.pattern_id !== "string") {
136
+ throw new Error("pattern_id deve ser string");
137
+ }
138
+ if (input.escala_fluxo !== undefined) {
139
+ ensurePositiveNumber("escala_fluxo", input.escala_fluxo);
140
+ }
141
+ if (input.deslocamento_meses !== undefined) {
142
+ ensureInteger("deslocamento_meses", input.deslocamento_meses);
143
+ }
144
+ }
145
+
146
+ export function montarFluxoClienteScp(input) {
147
+ validateScpFlowInput(input);
148
+
149
+ const client = findClient(input.client_id);
150
+ if (!client) {
151
+ throw new Error(`cliente nao encontrado: ${input.client_id}`);
152
+ }
153
+
154
+ const selectedPatternId = input.pattern_id || client.default_pattern_id;
155
+ const pattern = findPattern(selectedPatternId);
156
+ if (!pattern) {
157
+ throw new Error(`padrao de fluxo nao encontrado: ${selectedPatternId}`);
158
+ }
159
+
160
+ const escalaFluxo = input.escala_fluxo !== undefined ? input.escala_fluxo : 1;
161
+ const deslocamentoMeses =
162
+ input.deslocamento_meses !== undefined ? input.deslocamento_meses : 0;
163
+ const novoClientId =
164
+ input.novo_client_id && String(input.novo_client_id).trim()
165
+ ? String(input.novo_client_id).trim()
166
+ : client.client_id;
167
+ const novoClientName =
168
+ input.novo_client_name && String(input.novo_client_name).trim()
169
+ ? String(input.novo_client_name).trim()
170
+ : client.client_name;
171
+
172
+ const pontos = pattern.pontos.map((point) => ({
173
+ periodo:
174
+ deslocamentoMeses === 0
175
+ ? point.periodo
176
+ : addMonths(point.periodo, deslocamentoMeses),
177
+ fluxo: point.fluxo * escalaFluxo,
178
+ }));
179
+ const fluxosMensais = pontos.map((point) => point.fluxo);
180
+
181
+ return {
182
+ client_id: novoClientId,
183
+ client_name: novoClientName,
184
+ source_client_id: client.client_id,
185
+ pattern_id: pattern.id,
186
+ pattern_name: pattern.nome,
187
+ escala_fluxo: escalaFluxo,
188
+ deslocamento_meses: deslocamentoMeses,
189
+ pontos,
190
+ fluxos_mensais: fluxosMensais,
191
+ };
192
+ }
193
+
194
+ export function calcularSimulacao(input) {
195
+ validateInput(input);
196
+
197
+ const { credito_desejado, parametros, tem_lance, lance_percentual } = input;
198
+ const {
199
+ prazo_meses,
200
+ taxa_administracao_total,
201
+ fundo_reserva_mensal,
202
+ seguro_prestamista,
203
+ } = parametros;
204
+
205
+ const fcMensal = 1.0 / prazo_meses;
206
+ const taMensal = taxa_administracao_total / prazo_meses;
207
+
208
+ const valorFC = credito_desejado * fcMensal;
209
+ const valorTA = credito_desejado * taMensal;
210
+ const valorFR = credito_desejado * fundo_reserva_mensal;
211
+
212
+ const valorBaseSeguro =
213
+ credito_desejado + credito_desejado * taxa_administracao_total;
214
+ const taxaSeguroMensal = seguro_prestamista / prazo_meses;
215
+ const valorSeguro = valorBaseSeguro * taxaSeguroMensal;
216
+
217
+ const parcelaIntegral = {
218
+ fundo_comum: valorFC,
219
+ taxa_administracao: valorTA,
220
+ fundo_reserva: valorFR,
221
+ seguro: valorSeguro,
222
+ total: valorFC + valorTA + valorFR + valorSeguro,
223
+ };
224
+
225
+ const fatorReducao = 0.7;
226
+ const valorFCReduzido = valorFC * fatorReducao;
227
+
228
+ const parcelaReduzida = {
229
+ fundo_comum: valorFCReduzido,
230
+ taxa_administracao: valorTA,
231
+ fundo_reserva: valorFR,
232
+ seguro: valorSeguro,
233
+ total: valorFCReduzido + valorTA + valorFR + valorSeguro,
234
+ };
235
+
236
+ let valorLance = 0;
237
+ let prazoAposLance = prazo_meses;
238
+
239
+ if (tem_lance && lance_percentual && lance_percentual > 0) {
240
+ valorLance = credito_desejado * lance_percentual;
241
+ const numeroParcelasAntecipadas = Math.floor(valorLance / parcelaIntegral.total);
242
+ prazoAposLance = Math.max(1, prazo_meses - numeroParcelasAntecipadas);
243
+ }
244
+
245
+ return {
246
+ credito: credito_desejado,
247
+ prazo: prazo_meses,
248
+ parcela_integral: parcelaIntegral,
249
+ parcela_reduzida: parcelaReduzida,
250
+ valor_lance: tem_lance ? valorLance : undefined,
251
+ prazo_apos_lance: tem_lance ? prazoAposLance : undefined,
252
+ };
253
+ }
254
+
255
+ function getSeguroRateByProduto(produto) {
256
+ const key = normalizeLookupKey(produto);
257
+ if (key === "imovel") {
258
+ return 0.0003245;
259
+ }
260
+ if (key === "automovel") {
261
+ return 0.0008697;
262
+ }
263
+ return 0;
264
+ }
265
+
266
+ function validateRodobensParityInput(input) {
267
+ if (!input || typeof input !== "object") {
268
+ throw new Error("input de paridade rodobens invalido");
269
+ }
270
+ if (!["degrau", "linear", "pontual"].includes(input.plan)) {
271
+ throw new Error("plan deve ser degrau, linear ou pontual");
272
+ }
273
+ ensurePositiveNumber("credito", input.credito);
274
+ ensurePositiveNumber("prazo_cliente", input.prazo_cliente);
275
+ ensureNonNegativeNumber("taxa_administrativa_total", input.taxa_administrativa_total);
276
+ if (input.numero_cotas !== undefined) {
277
+ ensurePositiveNumber("numero_cotas", input.numero_cotas);
278
+ }
279
+ if (input.furo !== undefined) {
280
+ ensureNonNegativeNumber("furo", input.furo);
281
+ }
282
+ if (input.embutido_percentual !== undefined) {
283
+ ensureNonNegativeNumber("embutido_percentual", input.embutido_percentual);
284
+ }
285
+ if (input.parcelas_lance !== undefined) {
286
+ ensureNonNegativeNumber("parcelas_lance", input.parcelas_lance);
287
+ }
288
+ if (input.mes_previsto_lance !== undefined) {
289
+ ensureNonNegativeNumber("mes_previsto_lance", input.mes_previsto_lance);
290
+ }
291
+ }
292
+
293
+ function buildDegrauParityContext(input) {
294
+ const credito = input.credito;
295
+ const prazo = input.prazo_cliente;
296
+ const taxaAdmTotal = input.taxa_administrativa_total;
297
+ const numeroCotas = input.numero_cotas ?? 1;
298
+ const embutidoPercentual = input.embutido_percentual ?? 0;
299
+ const parcelasLance = input.parcelas_lance ?? 0;
300
+ const mesPrevistoLance = input.mes_previsto_lance ?? 0;
301
+ const furo = input.furo ?? 0;
302
+ const plano = input.plano || "Integral";
303
+ const tipoLance = input.tipo_lance || "Lance parcela Int";
304
+ const tipoProduto = input.tipo_produto || "Imóvel";
305
+ const seguroRate =
306
+ normalizeLookupKey(input.seguro_prestamista_habilitado) === "nao" ||
307
+ normalizeLookupKey(input.seguro_prestamista_habilitado) === "não"
308
+ ? 0
309
+ : getSeguroRateByProduto(tipoProduto);
310
+ const reductionFactor = plano.includes("50%")
311
+ ? 0.5
312
+ : plano.includes("70%")
313
+ ? 0.7
314
+ : 1;
315
+
316
+ const parcelaSeguro = ((credito * taxaAdmTotal) + credito) * seguroRate;
317
+ const parcelaIntegralPrimeiraMetade =
318
+ credito / prazo + (credito * taxaAdmTotal) / (prazo / 2) + parcelaSeguro;
319
+ const parcelaIntegralSegundaMetade = credito / prazo + parcelaSeguro;
320
+ const parcelaReduzidaPrimeiraMetade =
321
+ ((credito / prazo) + (credito * taxaAdmTotal) / (prazo / 2)) * reductionFactor +
322
+ parcelaSeguro;
323
+ const parcelaReduzidaSegundaMetade =
324
+ (credito / prazo) * reductionFactor + parcelaSeguro;
325
+ const primeiraParcelaPorCota =
326
+ plano === "Integral"
327
+ ? parcelaIntegralPrimeiraMetade
328
+ : parcelaReduzidaPrimeiraMetade;
329
+ const lanceEmbutidoValor = credito * embutidoPercentual;
330
+ const usaLanceReduzido = normalizeLookupKey(tipoLance).includes("red");
331
+ const parcelaPrimeiraMetadeParaLance = usaLanceReduzido
332
+ ? parcelaReduzidaPrimeiraMetade
333
+ : parcelaIntegralPrimeiraMetade;
334
+ const parcelaSegundaMetadeParaLance = usaLanceReduzido
335
+ ? parcelaReduzidaSegundaMetade
336
+ : parcelaIntegralSegundaMetade;
337
+ const metadePrazo = prazo / 2;
338
+ const lanceTotalPorCota =
339
+ parcelasLance > metadePrazo
340
+ ? metadePrazo * parcelaSegundaMetadeParaLance +
341
+ (parcelasLance - metadePrazo) * parcelaPrimeiraMetadeParaLance
342
+ : parcelasLance * parcelaSegundaMetadeParaLance;
343
+ const lanceTotal = lanceTotalPorCota * numeroCotas;
344
+ const recursoProprio = lanceTotal - lanceEmbutidoValor * numeroCotas;
345
+ const creditoDisponivel = (credito - lanceEmbutidoValor) * numeroCotas;
346
+
347
+ return {
348
+ partner_name: input.partner_name ?? null,
349
+ plan_label: `Degrau ${plano}`,
350
+ numero_cotas: numeroCotas,
351
+ credito_contratado: credito * numeroCotas,
352
+ lance_total: lanceTotal,
353
+ lance_embutido_valor: lanceEmbutidoValor * numeroCotas,
354
+ recurso_proprio: recursoProprio,
355
+ primeira_parcela: primeiraParcelaPorCota * numeroCotas,
356
+ credito_disponivel: creditoDisponivel,
357
+ credito_liquido: creditoDisponivel,
358
+ furo,
359
+ mes_de_contemplacao: mesPrevistoLance,
360
+ seguro_prestamista: seguroRate,
361
+ fundo_reserva: 0,
362
+ taxa_administracao_exibida: `${formatNumberWithComma(
363
+ roundTo((taxaAdmTotal / prazo) * 100, 6),
364
+ 4,
365
+ )} % a.m.`,
366
+ taxa_adesao: 0,
367
+ diluicao_50_texto: null,
368
+ option_title: null,
369
+ saldo_devedor_total: null,
370
+ };
371
+ }
372
+
373
+ function buildLinearParityContext(input) {
374
+ const taxaAdmTotal = input.taxa_administrativa_total;
375
+ const prazo = input.prazo_cliente;
376
+ const plano = input.plano || "Linear Integral";
377
+ const tipoProduto = input.tipo_produto || "Imóvel";
378
+ const seguroRate =
379
+ normalizeLookupKey(input.seguro_prestamista_habilitado) === "sim"
380
+ ? getSeguroRateByProduto(tipoProduto)
381
+ : 0;
382
+
383
+ return {
384
+ partner_name: input.partner_name || null,
385
+ furo: input.furo ?? 0,
386
+ seguro_prestamista: seguroRate,
387
+ grupo: input.grupo ?? null,
388
+ plan_label: plano,
389
+ numero_cotas: input.numero_cotas ?? null,
390
+ taxa_administracao_exibida: `${roundTo((taxaAdmTotal / prazo) * 100, 5).toFixed(
391
+ 5,
392
+ )} % a.m.`,
393
+ mes_de_contemplacao: input.mes_previsto_lance ?? null,
394
+ texto_diluicao_50:
395
+ plano === "Linear Integral" ? "" : "Plano de Parcela Reduzida até a contemplação",
396
+ credito_contratado: null,
397
+ lance_total: null,
398
+ lance_embutido_valor: null,
399
+ recurso_proprio: null,
400
+ primeira_parcela: null,
401
+ credito_disponivel: null,
402
+ credito_liquido: null,
403
+ taxa_adesao: null,
404
+ option_title: null,
405
+ saldo_devedor_total: null,
406
+ };
407
+ }
408
+
409
+ function buildPontualParityContext(input) {
410
+ const produtoBaseControle = input.produto_base_controle || input.tipo_produto || "Automóvel";
411
+ const saldoBase = input.credito * (1 + input.taxa_administrativa_total);
412
+ const seguroRate = getSeguroRateByProduto(produtoBaseControle);
413
+ const saldoDevedorTotal = saldoBase * (1 + input.prazo_cliente * seguroRate);
414
+ const usaBranchAuto = normalizeLookupKey(produtoBaseControle) === "automovel";
415
+
416
+ return {
417
+ partner_name: input.partner_name || null,
418
+ option_title: usaBranchAuto
419
+ ? "Contemplação por Sorteio (Até 12 Meses)"
420
+ : "Contemplação por Sorteio (Até 24 Meses)",
421
+ furo: input.furo ?? 0,
422
+ plan_label: usaBranchAuto ? "Plano Pontual - Auto" : "Plano Pontual - Imóvel",
423
+ numero_cotas: input.numero_cotas ?? null,
424
+ taxa_adesao: 0,
425
+ fundo_reserva: 0,
426
+ saldo_devedor_total: saldoDevedorTotal,
427
+ credito_contratado: null,
428
+ lance_total: null,
429
+ lance_embutido_valor: null,
430
+ recurso_proprio: null,
431
+ primeira_parcela: null,
432
+ credito_disponivel: null,
433
+ credito_liquido: null,
434
+ grupo: input.grupo ?? null,
435
+ mes_de_contemplacao: null,
436
+ seguro_prestamista: null,
437
+ taxa_administracao_exibida: null,
438
+ texto_diluicao_50: null,
439
+ };
440
+ }
441
+
442
+ function getRodobensParityMatrixRows(plan) {
443
+ const rows = RODOBENS_FIELD_MATRIX_BY_PLAN.get(plan) || [];
444
+ return rows.filter((row) => {
445
+ if (row.status === "pending") {
446
+ return false;
447
+ }
448
+ if (row.layout_cell) {
449
+ return true;
450
+ }
451
+ return row.field_id === "saldo_devedor_total";
452
+ });
453
+ }
454
+
455
+ function resolveRodobensParityField(row, input, context) {
456
+ const outputFieldId = RODOBENS_PARITY_FIELD_ALIASES[row.field_id] || row.field_id;
457
+ if (Object.hasOwn(context, outputFieldId)) {
458
+ return context[outputFieldId];
459
+ }
460
+ if (Object.hasOwn(context, row.field_id)) {
461
+ return context[row.field_id];
462
+ }
463
+ if (Object.hasOwn(input, outputFieldId)) {
464
+ return input[outputFieldId];
465
+ }
466
+ if (Object.hasOwn(input, row.field_id)) {
467
+ return input[row.field_id];
468
+ }
469
+ if (row.current_value !== undefined && row.current_value !== null && row.status === "review") {
470
+ return row.current_value;
471
+ }
472
+ return undefined;
473
+ }
474
+
475
+ export function calcularParidadeRodobens(input) {
476
+ validateRodobensParityInput(input);
477
+
478
+ let context;
479
+ if (input.plan === "degrau") {
480
+ context = buildDegrauParityContext(input);
481
+ } else if (input.plan === "linear") {
482
+ context = buildLinearParityContext(input);
483
+ } else {
484
+ context = buildPontualParityContext(input);
485
+ }
486
+
487
+ const result = {};
488
+ for (const row of getRodobensParityMatrixRows(input.plan)) {
489
+ const value = resolveRodobensParityField(row, input, context);
490
+ const outputFieldId = RODOBENS_PARITY_FIELD_ALIASES[row.field_id] || row.field_id;
491
+ if (value !== undefined) {
492
+ result[outputFieldId] = value;
493
+ }
494
+ }
495
+ return result;
496
+ }
497
+
498
+ function npv(rate, cashflows) {
499
+ let value = 0;
500
+ for (let i = 0; i < cashflows.length; i += 1) {
501
+ value += cashflows[i] / (1 + rate) ** i;
502
+ }
503
+ return value;
504
+ }
505
+
506
+ function hasSignChange(cashflows) {
507
+ let hasPositive = false;
508
+ let hasNegative = false;
509
+ for (const value of cashflows) {
510
+ if (value > 0) {
511
+ hasPositive = true;
512
+ }
513
+ if (value < 0) {
514
+ hasNegative = true;
515
+ }
516
+ if (hasPositive && hasNegative) {
517
+ return true;
518
+ }
519
+ }
520
+ return false;
521
+ }
522
+
523
+ function calculateIrrMonthly(cashflows) {
524
+ if (!hasSignChange(cashflows)) {
525
+ return null;
526
+ }
527
+
528
+ let low = -0.9999;
529
+ let high = 10;
530
+ let npvLow = npv(low, cashflows);
531
+ let npvHigh = npv(high, cashflows);
532
+
533
+ if (npvLow === 0) {
534
+ return low;
535
+ }
536
+ if (npvHigh === 0) {
537
+ return high;
538
+ }
539
+
540
+ let attempts = 0;
541
+ while (npvLow * npvHigh > 0 && attempts < 50) {
542
+ high *= 2;
543
+ npvHigh = npv(high, cashflows);
544
+ attempts += 1;
545
+ }
546
+
547
+ if (npvLow * npvHigh > 0) {
548
+ return null;
549
+ }
550
+
551
+ for (let i = 0; i < 200; i += 1) {
552
+ const mid = (low + high) / 2;
553
+ const npvMid = npv(mid, cashflows);
554
+ if (Math.abs(npvMid) < 1e-8) {
555
+ return mid;
556
+ }
557
+ if (npvLow * npvMid < 0) {
558
+ high = mid;
559
+ npvHigh = npvMid;
560
+ } else {
561
+ low = mid;
562
+ npvLow = npvMid;
563
+ }
564
+ }
565
+
566
+ return (low + high) / 2;
567
+ }
568
+
569
+ function validateSecondaryInput(input) {
570
+ if (!input || typeof input !== "object") {
571
+ throw new Error("input secundario invalido");
572
+ }
573
+ ensureNumberArray("fluxos_mensais", input.fluxos_mensais);
574
+
575
+ if (input.portabilidade_percentual_contemplada !== undefined) {
576
+ ensurePercent(
577
+ "portabilidade_percentual_contemplada",
578
+ input.portabilidade_percentual_contemplada
579
+ );
580
+ }
581
+ if (input.portabilidade_percentual_nao_contemplada !== undefined) {
582
+ ensurePercent(
583
+ "portabilidade_percentual_nao_contemplada",
584
+ input.portabilidade_percentual_nao_contemplada
585
+ );
586
+ }
587
+ if (input.fee_percentual !== undefined) {
588
+ ensurePercent("fee_percentual", input.fee_percentual);
589
+ }
590
+ if (input.percentual_cartas_parcela_reduzida !== undefined) {
591
+ ensurePercent(
592
+ "percentual_cartas_parcela_reduzida",
593
+ input.percentual_cartas_parcela_reduzida
594
+ );
595
+ }
596
+ if (input.volume_contratado !== undefined) {
597
+ ensureNonNegativeNumber("volume_contratado", input.volume_contratado);
598
+ }
599
+ if (input.meses_pos_ultima_contemplacao !== undefined) {
600
+ ensureNonNegativeNumber(
601
+ "meses_pos_ultima_contemplacao",
602
+ input.meses_pos_ultima_contemplacao
603
+ );
604
+ }
605
+
606
+ if (input.transacoes_portabilidade !== undefined) {
607
+ if (!Array.isArray(input.transacoes_portabilidade)) {
608
+ throw new Error("transacoes_portabilidade deve ser uma lista");
609
+ }
610
+ for (const transacao of input.transacoes_portabilidade) {
611
+ if (!transacao || typeof transacao !== "object") {
612
+ throw new Error("transacoes_portabilidade contem item invalido");
613
+ }
614
+ ensureNonNegativeNumber("transacao.valor", transacao.valor);
615
+ if (typeof transacao.contemplada !== "boolean") {
616
+ throw new Error("transacao.contemplada deve ser boolean");
617
+ }
618
+ }
619
+ }
620
+ }
621
+
622
+ export function calcularIndicadoresSecundario(input) {
623
+ validateSecondaryInput(input);
624
+
625
+ const portabilidadeContemplada =
626
+ input.portabilidade_percentual_contemplada !== undefined
627
+ ? input.portabilidade_percentual_contemplada
628
+ : 0.01;
629
+ const portabilidadeNaoContemplada =
630
+ input.portabilidade_percentual_nao_contemplada !== undefined
631
+ ? input.portabilidade_percentual_nao_contemplada
632
+ : 0.005;
633
+ const feePercentual = input.fee_percentual !== undefined ? input.fee_percentual : 0;
634
+ const volumeContratado =
635
+ input.volume_contratado !== undefined ? input.volume_contratado : 0;
636
+ const percentualCartasParcelaReduzida =
637
+ input.percentual_cartas_parcela_reduzida !== undefined
638
+ ? input.percentual_cartas_parcela_reduzida
639
+ : 0;
640
+ const mesesPosUltimaContemplacao =
641
+ input.meses_pos_ultima_contemplacao !== undefined
642
+ ? input.meses_pos_ultima_contemplacao
643
+ : 0;
644
+
645
+ let custoPortabilidade = 0;
646
+ const transacoes = input.transacoes_portabilidade || [];
647
+ for (const transacao of transacoes) {
648
+ const taxa = transacao.contemplada
649
+ ? portabilidadeContemplada
650
+ : portabilidadeNaoContemplada;
651
+ custoPortabilidade += transacao.valor * taxa;
652
+ }
653
+
654
+ const fatorFeeVolume =
655
+ (1 - percentualCartasParcelaReduzida) + percentualCartasParcelaReduzida * 0.7;
656
+ const custoFeeParcelaReduzida = volumeContratado * fatorFeeVolume * feePercentual;
657
+ const custoTotalDeducoes = custoPortabilidade + custoFeeParcelaReduzida;
658
+
659
+ const fluxosAjustados = [...input.fluxos_mensais];
660
+ fluxosAjustados[0] -= custoTotalDeducoes;
661
+
662
+ let acumulado = 0;
663
+ let caixaMaximoNegativo = 0;
664
+ let paybackBase = null;
665
+
666
+ for (let i = 0; i < fluxosAjustados.length; i += 1) {
667
+ acumulado += fluxosAjustados[i];
668
+ if (acumulado < caixaMaximoNegativo) {
669
+ caixaMaximoNegativo = acumulado;
670
+ }
671
+ if (paybackBase === null && acumulado >= 0) {
672
+ paybackBase = i + 1;
673
+ }
674
+ }
675
+
676
+ const tirMensal = calculateIrrMonthly(fluxosAjustados);
677
+ const tirAnual = tirMensal === null ? null : (1 + tirMensal) ** 12 - 1;
678
+
679
+ const paybackMeses =
680
+ paybackBase === null ? null : paybackBase + mesesPosUltimaContemplacao;
681
+
682
+ return {
683
+ fluxo_ajustado: fluxosAjustados,
684
+ custo_portabilidade: custoPortabilidade,
685
+ custo_fee_parcela_reduzida: custoFeeParcelaReduzida,
686
+ custo_total_deducoes: custoTotalDeducoes,
687
+ caixa_maximo_negativo: caixaMaximoNegativo,
688
+ payback_meses: paybackMeses,
689
+ tir_mensal: tirMensal,
690
+ tir_anual: tirAnual,
691
+ regras_aplicadas: {
692
+ portabilidade_contemplada: portabilidadeContemplada,
693
+ portabilidade_nao_contemplada: portabilidadeNaoContemplada,
694
+ sem_correcao_antes_contemplacao:
695
+ input.sem_correcao_antes_contemplacao !== undefined
696
+ ? Boolean(input.sem_correcao_antes_contemplacao)
697
+ : true,
698
+ fee_sobre_volume_reduzido_70: true,
699
+ },
700
+ };
701
+ }
702
+
703
+ export function simularSecundarioComPerfilScp(input) {
704
+ if (!input || typeof input !== "object") {
705
+ throw new Error("input de simulacao scp invalido");
706
+ }
707
+ const fluxoCliente = montarFluxoClienteScp({
708
+ client_id: input.client_id,
709
+ pattern_id: input.pattern_id,
710
+ escala_fluxo: input.escala_fluxo,
711
+ deslocamento_meses: input.deslocamento_meses,
712
+ novo_client_id: input.novo_client_id,
713
+ novo_client_name: input.novo_client_name,
714
+ });
715
+
716
+ const simulacao = calcularIndicadoresSecundario({
717
+ fluxos_mensais: fluxoCliente.fluxos_mensais,
718
+ transacoes_portabilidade: input.transacoes_portabilidade,
719
+ portabilidade_percentual_contemplada:
720
+ input.portabilidade_percentual_contemplada,
721
+ portabilidade_percentual_nao_contemplada:
722
+ input.portabilidade_percentual_nao_contemplada,
723
+ volume_contratado: input.volume_contratado,
724
+ fee_percentual: input.fee_percentual,
725
+ percentual_cartas_parcela_reduzida:
726
+ input.percentual_cartas_parcela_reduzida,
727
+ meses_pos_ultima_contemplacao: input.meses_pos_ultima_contemplacao,
728
+ sem_correcao_antes_contemplacao: input.sem_correcao_antes_contemplacao,
729
+ });
730
+
731
+ return {
732
+ ...simulacao,
733
+ fluxo_cliente: fluxoCliente,
734
+ };
735
+ }
736
+
737
+ function validateConversaoInput(input) {
738
+ if (!input || typeof input !== "object") {
739
+ throw new Error("input de conversao invalido");
740
+ }
741
+ if (input.valor_imovel_base !== undefined) {
742
+ ensurePositiveNumber("valor_imovel_base", input.valor_imovel_base);
743
+ }
744
+ if (input.desconto_imovel_percentual !== undefined) {
745
+ ensurePercent("desconto_imovel_percentual", input.desconto_imovel_percentual);
746
+ }
747
+ if (input.percentual_entrada_imovel !== undefined) {
748
+ ensurePercent("percentual_entrada_imovel", input.percentual_entrada_imovel);
749
+ }
750
+ if (input.agio_consorcio_percentual !== undefined) {
751
+ ensurePercent("agio_consorcio_percentual", input.agio_consorcio_percentual);
752
+ }
753
+ if (input.taxa_custos_transacao_percentual !== undefined) {
754
+ ensurePercent(
755
+ "taxa_custos_transacao_percentual",
756
+ input.taxa_custos_transacao_percentual
757
+ );
758
+ }
759
+ }
760
+
761
+ export function simularModoConversaoScpConsorcio(input) {
762
+ validateConversaoInput(input);
763
+
764
+ const secundario = simularSecundarioComPerfilScp(input);
765
+ const valorImovelBase =
766
+ input.valor_imovel_base !== undefined ? input.valor_imovel_base : 500000;
767
+ const descontoImovelPercentual =
768
+ input.desconto_imovel_percentual !== undefined
769
+ ? input.desconto_imovel_percentual
770
+ : 0.15;
771
+ const percentualEntradaImovel =
772
+ input.percentual_entrada_imovel !== undefined
773
+ ? input.percentual_entrada_imovel
774
+ : 0.2;
775
+ const agioConsorcioPercentual =
776
+ input.agio_consorcio_percentual !== undefined
777
+ ? input.agio_consorcio_percentual
778
+ : 0.12;
779
+ const taxaCustosTransacaoPercentual =
780
+ input.taxa_custos_transacao_percentual !== undefined
781
+ ? input.taxa_custos_transacao_percentual
782
+ : 0;
783
+
784
+ const valorImovelComDesconto =
785
+ valorImovelBase * (1 - descontoImovelPercentual);
786
+ const valorEntrada = valorImovelComDesconto * percentualEntradaImovel;
787
+ const valorCreditoConsorcio =
788
+ valorImovelComDesconto - valorEntrada;
789
+ const valorCustosTransacao =
790
+ valorImovelComDesconto * taxaCustosTransacaoPercentual;
791
+ const investimentoTotalConversao =
792
+ valorEntrada + valorCustosTransacao + secundario.custo_total_deducoes;
793
+ const valorSaidaConsorcioSecundario =
794
+ valorCreditoConsorcio * (1 + agioConsorcioPercentual);
795
+ const lucroProjetadoConversao =
796
+ valorSaidaConsorcioSecundario - investimentoTotalConversao;
797
+ const roiProjetadoConversao =
798
+ investimentoTotalConversao > 0
799
+ ? lucroProjetadoConversao / investimentoTotalConversao
800
+ : null;
801
+
802
+ return {
803
+ ...secundario,
804
+ modo_conversao: {
805
+ natureza_cenario: "fechado_customizavel",
806
+ observacao:
807
+ "Cenario de conversao SCP para compra de imovel com desconto e operacao no secundario.",
808
+ valor_imovel_base: valorImovelBase,
809
+ desconto_imovel_percentual: descontoImovelPercentual,
810
+ percentual_entrada_imovel: percentualEntradaImovel,
811
+ agio_consorcio_percentual: agioConsorcioPercentual,
812
+ taxa_custos_transacao_percentual: taxaCustosTransacaoPercentual,
813
+ valor_imovel_com_desconto: valorImovelComDesconto,
814
+ valor_entrada_imovel: valorEntrada,
815
+ valor_credito_consorcio: valorCreditoConsorcio,
816
+ valor_custos_transacao: valorCustosTransacao,
817
+ investimento_total_conversao: investimentoTotalConversao,
818
+ valor_saida_consorcio_secundario: valorSaidaConsorcioSecundario,
819
+ lucro_projetado_conversao: lucroProjetadoConversao,
820
+ roi_projetado_conversao: roiProjetadoConversao,
821
+ },
822
+ };
823
+ }
packages/simulation-engine/src/scpDefaults.js ADDED
@@ -0,0 +1,658 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const EMPTY_FLOW_PATTERN = {
2
+ id: "scp_sem_fluxo",
3
+ nome: "SCP sem fluxo histórico",
4
+ descricao: "Template vazio para clientes sem histórico carregado.",
5
+ pontos: [{ periodo: "2021-01-01", fluxo: 0 }],
6
+ };
7
+
8
+ const EXTRATO_6_FLOW_PATTERN = {
9
+ id: "scp_extrato_6_consolidado",
10
+ nome: "SCP Extrato Investidor 6 (Consolidado)",
11
+ descricao: "Fluxo consolidado mensal normalizado do investidor 6 (2021-02 a 2034-01).",
12
+ pontos: [
13
+ {
14
+ "periodo": "2021-02-01",
15
+ "fluxo": -737564.52
16
+ },
17
+ {
18
+ "periodo": "2021-03-01",
19
+ "fluxo": -89500.47
20
+ },
21
+ {
22
+ "periodo": "2021-04-01",
23
+ "fluxo": -98532.08
24
+ },
25
+ {
26
+ "periodo": "2021-06-01",
27
+ "fluxo": -93528.63
28
+ },
29
+ {
30
+ "periodo": "2021-07-01",
31
+ "fluxo": -92248.24
32
+ },
33
+ {
34
+ "periodo": "2021-09-01",
35
+ "fluxo": -89535.06
36
+ },
37
+ {
38
+ "periodo": "2021-10-01",
39
+ "fluxo": -88098.84
40
+ },
41
+ {
42
+ "periodo": "2021-12-01",
43
+ "fluxo": -156634.54
44
+ },
45
+ {
46
+ "periodo": "2022-02-01",
47
+ "fluxo": -150588.0
48
+ },
49
+ {
50
+ "periodo": "2022-03-01",
51
+ "fluxo": -50408.16
52
+ },
53
+ {
54
+ "periodo": "2022-04-01",
55
+ "fluxo": -29554.74
56
+ },
57
+ {
58
+ "periodo": "2022-05-01",
59
+ "fluxo": -618506.94
60
+ },
61
+ {
62
+ "periodo": "2022-06-01",
63
+ "fluxo": -736408.34
64
+ },
65
+ {
66
+ "periodo": "2022-07-01",
67
+ "fluxo": -188867.66
68
+ },
69
+ {
70
+ "periodo": "2022-08-01",
71
+ "fluxo": -95358.15
72
+ },
73
+ {
74
+ "periodo": "2022-09-01",
75
+ "fluxo": -224642.19
76
+ },
77
+ {
78
+ "periodo": "2022-10-01",
79
+ "fluxo": -281891.02
80
+ },
81
+ {
82
+ "periodo": "2022-11-01",
83
+ "fluxo": -693594.29
84
+ },
85
+ {
86
+ "periodo": "2022-12-01",
87
+ "fluxo": -499447.91
88
+ },
89
+ {
90
+ "periodo": "2023-01-01",
91
+ "fluxo": -646374.71
92
+ },
93
+ {
94
+ "periodo": "2023-02-01",
95
+ "fluxo": -919693.27
96
+ },
97
+ {
98
+ "periodo": "2023-03-01",
99
+ "fluxo": -1067632.66
100
+ },
101
+ {
102
+ "periodo": "2023-04-01",
103
+ "fluxo": -992735.29
104
+ },
105
+ {
106
+ "periodo": "2023-05-01",
107
+ "fluxo": -445363.53
108
+ },
109
+ {
110
+ "periodo": "2023-06-01",
111
+ "fluxo": -296225.14
112
+ },
113
+ {
114
+ "periodo": "2023-07-01",
115
+ "fluxo": -251036.26
116
+ },
117
+ {
118
+ "periodo": "2023-08-01",
119
+ "fluxo": -187543.36
120
+ },
121
+ {
122
+ "periodo": "2023-09-01",
123
+ "fluxo": -255700.26
124
+ },
125
+ {
126
+ "periodo": "2023-10-01",
127
+ "fluxo": -23630.06
128
+ },
129
+ {
130
+ "periodo": "2023-11-01",
131
+ "fluxo": -186845.68
132
+ },
133
+ {
134
+ "periodo": "2023-12-01",
135
+ "fluxo": -145086.8
136
+ },
137
+ {
138
+ "periodo": "2024-01-01",
139
+ "fluxo": -20697.02
140
+ },
141
+ {
142
+ "periodo": "2024-02-01",
143
+ "fluxo": -122169.49
144
+ },
145
+ {
146
+ "periodo": "2024-03-01",
147
+ "fluxo": -73130.94
148
+ },
149
+ {
150
+ "periodo": "2024-04-01",
151
+ "fluxo": -125938.56
152
+ },
153
+ {
154
+ "periodo": "2024-07-01",
155
+ "fluxo": -39950.54
156
+ },
157
+ {
158
+ "periodo": "2024-08-01",
159
+ "fluxo": -61941.22
160
+ },
161
+ {
162
+ "periodo": "2024-09-01",
163
+ "fluxo": -54658.48
164
+ },
165
+ {
166
+ "periodo": "2024-10-01",
167
+ "fluxo": -225120.87
168
+ },
169
+ {
170
+ "periodo": "2024-11-01",
171
+ "fluxo": -125567.88
172
+ },
173
+ {
174
+ "periodo": "2024-12-01",
175
+ "fluxo": -124604.04
176
+ },
177
+ {
178
+ "periodo": "2025-01-01",
179
+ "fluxo": -162232.609879
180
+ },
181
+ {
182
+ "periodo": "2025-02-01",
183
+ "fluxo": 9840.556197
184
+ },
185
+ {
186
+ "periodo": "2025-03-01",
187
+ "fluxo": -432548.202122
188
+ },
189
+ {
190
+ "periodo": "2025-04-01",
191
+ "fluxo": -458262.952122
192
+ },
193
+ {
194
+ "periodo": "2025-05-01",
195
+ "fluxo": -423097.697657
196
+ },
197
+ {
198
+ "periodo": "2025-06-01",
199
+ "fluxo": -434612.579173
200
+ },
201
+ {
202
+ "periodo": "2025-07-01",
203
+ "fluxo": -121779.498649
204
+ },
205
+ {
206
+ "periodo": "2025-08-01",
207
+ "fluxo": -307297.083824
208
+ },
209
+ {
210
+ "periodo": "2025-09-01",
211
+ "fluxo": -329799.813629
212
+ },
213
+ {
214
+ "periodo": "2025-10-01",
215
+ "fluxo": -225956.066969
216
+ },
217
+ {
218
+ "periodo": "2025-11-01",
219
+ "fluxo": -288445.512606
220
+ },
221
+ {
222
+ "periodo": "2025-12-01",
223
+ "fluxo": -283414.556707
224
+ },
225
+ {
226
+ "periodo": "2026-01-01",
227
+ "fluxo": 527518.470288
228
+ },
229
+ {
230
+ "periodo": "2026-02-01",
231
+ "fluxo": 269150.845685
232
+ },
233
+ {
234
+ "periodo": "2026-03-01",
235
+ "fluxo": 1305376.352862
236
+ },
237
+ {
238
+ "periodo": "2026-04-01",
239
+ "fluxo": 280794.893435
240
+ },
241
+ {
242
+ "periodo": "2026-05-01",
243
+ "fluxo": 209821.215034
244
+ },
245
+ {
246
+ "periodo": "2026-06-01",
247
+ "fluxo": 60100.726869
248
+ },
249
+ {
250
+ "periodo": "2026-07-01",
251
+ "fluxo": 337614.567424
252
+ },
253
+ {
254
+ "periodo": "2026-08-01",
255
+ "fluxo": 294318.884378
256
+ },
257
+ {
258
+ "periodo": "2026-09-01",
259
+ "fluxo": -46141.362479
260
+ },
261
+ {
262
+ "periodo": "2026-10-01",
263
+ "fluxo": -55076.915784
264
+ },
265
+ {
266
+ "periodo": "2026-11-01",
267
+ "fluxo": 19929.808238
268
+ },
269
+ {
270
+ "periodo": "2026-12-01",
271
+ "fluxo": 500315.052052
272
+ },
273
+ {
274
+ "periodo": "2027-01-01",
275
+ "fluxo": -218977.546392
276
+ },
277
+ {
278
+ "periodo": "2027-02-01",
279
+ "fluxo": 994.423556
280
+ },
281
+ {
282
+ "periodo": "2027-03-01",
283
+ "fluxo": 404025.712056
284
+ },
285
+ {
286
+ "periodo": "2027-04-01",
287
+ "fluxo": 233575.22893
288
+ },
289
+ {
290
+ "periodo": "2027-05-01",
291
+ "fluxo": 238878.882716
292
+ },
293
+ {
294
+ "periodo": "2027-06-01",
295
+ "fluxo": 6990240.630296
296
+ },
297
+ {
298
+ "periodo": "2027-07-01",
299
+ "fluxo": 1874488.174107
300
+ },
301
+ {
302
+ "periodo": "2027-08-01",
303
+ "fluxo": 1113986.478002
304
+ },
305
+ {
306
+ "periodo": "2027-09-01",
307
+ "fluxo": 272380.708805
308
+ },
309
+ {
310
+ "periodo": "2027-10-01",
311
+ "fluxo": 1301052.16967
312
+ },
313
+ {
314
+ "periodo": "2027-11-01",
315
+ "fluxo": 1277465.729149
316
+ },
317
+ {
318
+ "periodo": "2027-12-01",
319
+ "fluxo": 1206630.595673
320
+ },
321
+ {
322
+ "periodo": "2028-01-01",
323
+ "fluxo": 1084333.681223
324
+ },
325
+ {
326
+ "periodo": "2028-02-01",
327
+ "fluxo": 79390.514072
328
+ },
329
+ {
330
+ "periodo": "2028-03-01",
331
+ "fluxo": 104139.639919
332
+ },
333
+ {
334
+ "periodo": "2028-04-01",
335
+ "fluxo": 34705.936188
336
+ },
337
+ {
338
+ "periodo": "2028-05-01",
339
+ "fluxo": 35063.791729
340
+ },
341
+ {
342
+ "periodo": "2028-06-01",
343
+ "fluxo": 35424.772993
344
+ },
345
+ {
346
+ "periodo": "2028-07-01",
347
+ "fluxo": 40009.831092
348
+ },
349
+ {
350
+ "periodo": "2028-08-01",
351
+ "fluxo": 40377.145945
352
+ },
353
+ {
354
+ "periodo": "2028-09-01",
355
+ "fluxo": 40747.669145
356
+ },
357
+ {
358
+ "periodo": "2028-10-01",
359
+ "fluxo": 41121.428714
360
+ },
361
+ {
362
+ "periodo": "2028-11-01",
363
+ "fluxo": 41498.452921
364
+ },
365
+ {
366
+ "periodo": "2028-12-01",
367
+ "fluxo": 40274.370333
368
+ },
369
+ {
370
+ "periodo": "2029-01-01",
371
+ "fluxo": 42262.40956
372
+ },
373
+ {
374
+ "periodo": "2029-02-01",
375
+ "fluxo": 43242.020582
376
+ },
377
+ {
378
+ "periodo": "2029-03-01",
379
+ "fluxo": 43632.390996
380
+ },
381
+ {
382
+ "periodo": "2029-04-01",
383
+ "fluxo": 44026.171137
384
+ },
385
+ {
386
+ "periodo": "2029-05-01",
387
+ "fluxo": 44423.390787
388
+ },
389
+ {
390
+ "periodo": "2029-06-01",
391
+ "fluxo": 44824.07999
392
+ },
393
+ {
394
+ "periodo": "2029-07-01",
395
+ "fluxo": 42992.830516
396
+ },
397
+ {
398
+ "periodo": "2029-08-01",
399
+ "fluxo": 44180.546937
400
+ },
401
+ {
402
+ "periodo": "2029-09-01",
403
+ "fluxo": 46015.260076
404
+ },
405
+ {
406
+ "periodo": "2029-10-01",
407
+ "fluxo": 46348.355526
408
+ },
409
+ {
410
+ "periodo": "2029-11-01",
411
+ "fluxo": 46680.813555
412
+ },
413
+ {
414
+ "periodo": "2029-12-01",
415
+ "fluxo": 40802.289293
416
+ },
417
+ {
418
+ "periodo": "2030-01-01",
419
+ "fluxo": 45820.242381
420
+ },
421
+ {
422
+ "periodo": "2030-02-01",
423
+ "fluxo": 46081.313866
424
+ },
425
+ {
426
+ "periodo": "2030-03-01",
427
+ "fluxo": 44637.236299
428
+ },
429
+ {
430
+ "periodo": "2030-04-01",
431
+ "fluxo": 38100.997039
432
+ },
433
+ {
434
+ "periodo": "2030-05-01",
435
+ "fluxo": 47917.662965
436
+ },
437
+ {
438
+ "periodo": "2030-06-01",
439
+ "fluxo": 48156.62759
440
+ },
441
+ {
442
+ "periodo": "2030-07-01",
443
+ "fluxo": 48396.751508
444
+ },
445
+ {
446
+ "periodo": "2030-08-01",
447
+ "fluxo": 48638.040343
448
+ },
449
+ {
450
+ "periodo": "2030-09-01",
451
+ "fluxo": 48883.754858
452
+ },
453
+ {
454
+ "periodo": "2030-10-01",
455
+ "fluxo": 49130.658876
456
+ },
457
+ {
458
+ "periodo": "2030-11-01",
459
+ "fluxo": 49375.476465
460
+ },
461
+ {
462
+ "periodo": "2030-12-01",
463
+ "fluxo": 49621.481732
464
+ },
465
+ {
466
+ "periodo": "2031-01-01",
467
+ "fluxo": 49276.059627
468
+ },
469
+ {
470
+ "periodo": "2031-02-01",
471
+ "fluxo": 49524.457559
472
+ },
473
+ {
474
+ "periodo": "2031-03-01",
475
+ "fluxo": 49774.060533
476
+ },
477
+ {
478
+ "periodo": "2031-04-01",
479
+ "fluxo": 50024.874392
480
+ },
481
+ {
482
+ "periodo": "2031-05-01",
483
+ "fluxo": 50276.90501
484
+ },
485
+ {
486
+ "periodo": "2031-06-01",
487
+ "fluxo": 50530.158287
488
+ },
489
+ {
490
+ "periodo": "2031-07-01",
491
+ "fluxo": 50784.640153
492
+ },
493
+ {
494
+ "periodo": "2031-08-01",
495
+ "fluxo": 51040.356565
496
+ },
497
+ {
498
+ "periodo": "2031-09-01",
499
+ "fluxo": 51300.731381
500
+ },
501
+ {
502
+ "periodo": "2031-10-01",
503
+ "fluxo": 51562.366671
504
+ },
505
+ {
506
+ "periodo": "2031-11-01",
507
+ "fluxo": 51821.822762
508
+ },
509
+ {
510
+ "periodo": "2031-12-01",
511
+ "fluxo": 52082.537521
512
+ },
513
+ {
514
+ "periodo": "2032-01-01",
515
+ "fluxo": 52344.517054
516
+ },
517
+ {
518
+ "periodo": "2032-02-01",
519
+ "fluxo": 52607.767494
520
+ },
521
+ {
522
+ "periodo": "2032-03-01",
523
+ "fluxo": 52872.295004
524
+ },
525
+ {
526
+ "periodo": "2032-04-01",
527
+ "fluxo": 53138.105778
528
+ },
529
+ {
530
+ "periodo": "2032-05-01",
531
+ "fluxo": 53405.206038
532
+ },
533
+ {
534
+ "periodo": "2032-06-01",
535
+ "fluxo": 53673.602039
536
+ },
537
+ {
538
+ "periodo": "2032-07-01",
539
+ "fluxo": 53943.300063
540
+ },
541
+ {
542
+ "periodo": "2032-08-01",
543
+ "fluxo": 54933.3961
544
+ },
545
+ {
546
+ "periodo": "2032-09-01",
547
+ "fluxo": 55209.305909
548
+ },
549
+ {
550
+ "periodo": "2032-10-01",
551
+ "fluxo": 55486.551398
552
+ },
553
+ {
554
+ "periodo": "2032-11-01",
555
+ "fluxo": 55765.139032
556
+ },
557
+ {
558
+ "periodo": "2032-12-01",
559
+ "fluxo": 56045.075308
560
+ },
561
+ {
562
+ "periodo": "2033-01-01",
563
+ "fluxo": 56918.987568
564
+ },
565
+ {
566
+ "periodo": "2033-02-01",
567
+ "fluxo": 57201.640748
568
+ },
569
+ {
570
+ "periodo": "2033-03-01",
571
+ "fluxo": 57485.662252
572
+ },
573
+ {
574
+ "periodo": "2033-04-01",
575
+ "fluxo": 59082.100043
576
+ },
577
+ {
578
+ "periodo": "2033-05-01",
579
+ "fluxo": 59526.20306
580
+ },
581
+ {
582
+ "periodo": "2033-06-01",
583
+ "fluxo": 59814.369409
584
+ },
585
+ {
586
+ "periodo": "2033-07-01",
587
+ "fluxo": 60103.930771
588
+ },
589
+ {
590
+ "periodo": "2033-08-01",
591
+ "fluxo": 60394.8939
592
+ },
593
+ {
594
+ "periodo": "2033-09-01",
595
+ "fluxo": 60687.265581
596
+ },
597
+ {
598
+ "periodo": "2033-10-01",
599
+ "fluxo": 60981.052634
600
+ },
601
+ {
602
+ "periodo": "2033-11-01",
603
+ "fluxo": 61276.26191
604
+ },
605
+ {
606
+ "periodo": "2033-12-01",
607
+ "fluxo": 61572.900294
608
+ },
609
+ {
610
+ "periodo": "2034-01-01",
611
+ "fluxo": 11858703.094597
612
+ }
613
+ ],
614
+ };
615
+
616
+ export const SCP_FLOW_PATTERNS_DEFAULT = [
617
+ EMPTY_FLOW_PATTERN,
618
+ EXTRATO_6_FLOW_PATTERN,
619
+ ];
620
+
621
+ export const SCP_CLIENTS_DEFAULT = [
622
+ {
623
+ client_id: "extrato_investidor_1",
624
+ client_name: "Extrato Investidor 1",
625
+ default_pattern_id: "scp_sem_fluxo",
626
+ status: "sem_fluxo_historico",
627
+ },
628
+ {
629
+ client_id: "extrato_investidor_2",
630
+ client_name: "Extrato Investidor 2",
631
+ default_pattern_id: "scp_sem_fluxo",
632
+ status: "sem_fluxo_historico",
633
+ },
634
+ {
635
+ client_id: "extrato_investidor_3",
636
+ client_name: "Extrato Investidor 3",
637
+ default_pattern_id: "scp_sem_fluxo",
638
+ status: "sem_fluxo_historico",
639
+ },
640
+ {
641
+ client_id: "extrato_investidor_4",
642
+ client_name: "Extrato Investidor 4",
643
+ default_pattern_id: "scp_sem_fluxo",
644
+ status: "sem_fluxo_historico",
645
+ },
646
+ {
647
+ client_id: "extrato_investidor_5",
648
+ client_name: "Extrato Investidor 5",
649
+ default_pattern_id: "scp_sem_fluxo",
650
+ status: "sem_fluxo_historico",
651
+ },
652
+ {
653
+ client_id: "extrato_investidor_6",
654
+ client_name: "Extrato Investidor 6",
655
+ default_pattern_id: "scp_extrato_6_consolidado",
656
+ status: "ativo",
657
+ },
658
+ ];
packages/simulation-engine/tests/simulation-engine.test.js ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { readFileSync } from "node:fs";
4
+ import {
5
+ calcularParidadeRodobens,
6
+ calcularIndicadoresSecundario,
7
+ calcularSimulacao,
8
+ listarPerfisFluxoScpDefault,
9
+ montarFluxoClienteScp,
10
+ simularModoConversaoScpConsorcio,
11
+ simularSecundarioComPerfilScp,
12
+ } from "../src/index.js";
13
+
14
+ const rodobensParityFixtures = JSON.parse(
15
+ readFileSync(
16
+ new URL("../../../tests/fixtures/rodobens_btg5_parity_fixtures_v1.json", import.meta.url),
17
+ "utf8",
18
+ ),
19
+ );
20
+
21
+ const rodobensPontualImovelSanitized = JSON.parse(
22
+ readFileSync(
23
+ new URL(
24
+ "../../../tests/fixtures/rodobens_btg5_pontual_imovel_sanitized_v1.json",
25
+ import.meta.url,
26
+ ),
27
+ "utf8",
28
+ ),
29
+ );
30
+
31
+ const baseInput = {
32
+ credito_desejado: 300000,
33
+ parametros: {
34
+ id_plano: "PADRAO",
35
+ nome_plano: "Plano Padrao Imovel",
36
+ prazo_meses: 180,
37
+ taxa_administracao_total: 0.15,
38
+ fundo_reserva_mensal: 0.0005,
39
+ seguro_prestamista: 0.035,
40
+ },
41
+ tem_lance: false,
42
+ };
43
+
44
+ function mapFields(entries) {
45
+ return Object.fromEntries(entries.map((entry) => [entry.field_id, entry.value]));
46
+ }
47
+
48
+ function buildRodobensParityInputFromScenario(scenario) {
49
+ const inputs = mapFields(scenario.inputs);
50
+
51
+ return {
52
+ plan: scenario.plan,
53
+ partner_name: inputs.partner_name,
54
+ grupo: inputs.grupo,
55
+ numero_cotas: inputs.numero_cotas,
56
+ modalidade_lance: inputs.modalidade_lance,
57
+ tipo_produto: inputs.tipo_produto,
58
+ produto_base_controle: inputs.produto_base_controle,
59
+ furo: inputs.furo,
60
+ credito: inputs.credito,
61
+ prazo_cliente: inputs.prazo_cliente,
62
+ taxa_administrativa_total: inputs.taxa_administrativa_total,
63
+ seguro_prestamista_habilitado: inputs.seguro_prestamista_habilitado,
64
+ plano: inputs.plano,
65
+ tipo_lance: inputs.tipo_lance,
66
+ embutido_percentual: inputs.embutido_percentual,
67
+ parcelas_lance: inputs.parcelas_lance,
68
+ mes_previsto_lance: inputs.mes_previsto_lance,
69
+ };
70
+ }
71
+
72
+ function assertParityOutputs(actual, expectedEntries) {
73
+ for (const expected of expectedEntries) {
74
+ assert.ok(
75
+ Object.hasOwn(actual, expected.field_id),
76
+ `campo ausente no resultado de paridade: ${expected.field_id}`,
77
+ );
78
+
79
+ const actualValue = actual[expected.field_id];
80
+ const expectedValue = expected.value;
81
+
82
+ if (typeof expectedValue === "number") {
83
+ assert.ok(
84
+ Math.abs(actualValue - expectedValue) < 1e-6,
85
+ `campo ${expected.field_id}: esperado ${expectedValue}, recebido ${actualValue}`,
86
+ );
87
+ continue;
88
+ }
89
+
90
+ assert.equal(
91
+ actualValue,
92
+ expectedValue,
93
+ `campo ${expected.field_id}: esperado ${expectedValue}, recebido ${actualValue}`,
94
+ );
95
+ }
96
+ }
97
+
98
+ test("calcula parcela integral com valores esperados", () => {
99
+ const result = calcularSimulacao(baseInput);
100
+
101
+ assert.equal(result.prazo, 180);
102
+ assert.equal(result.credito, 300000);
103
+ assert.ok(result.parcela_integral.total > 0);
104
+ assert.equal(Number(result.parcela_integral.fundo_comum.toFixed(2)), 1666.67);
105
+ });
106
+
107
+ test("calcula cenario com lance e reduz prazo", () => {
108
+ const result = calcularSimulacao({
109
+ ...baseInput,
110
+ tem_lance: true,
111
+ lance_percentual: 0.3,
112
+ });
113
+
114
+ assert.ok(result.valor_lance);
115
+ assert.equal(result.valor_lance, 90000);
116
+ assert.ok(result.prazo_apos_lance);
117
+ assert.ok(result.prazo_apos_lance < result.prazo);
118
+ });
119
+
120
+ test("falha quando prazo_meses for invalido", () => {
121
+ assert.throws(() => {
122
+ calcularSimulacao({
123
+ ...baseInput,
124
+ parametros: {
125
+ ...baseInput.parametros,
126
+ prazo_meses: 0,
127
+ },
128
+ });
129
+ }, /prazo_meses/);
130
+ });
131
+
132
+ test("calcula payback do secundario com offset apos ultima contemplacao", () => {
133
+ const fluxos = [];
134
+ for (let i = 0; i < 10; i += 1) {
135
+ fluxos.push(-100);
136
+ }
137
+ for (let i = 0; i < 50; i += 1) {
138
+ fluxos.push(20);
139
+ }
140
+
141
+ const result = calcularIndicadoresSecundario({
142
+ fluxos_mensais: fluxos,
143
+ meses_pos_ultima_contemplacao: 3,
144
+ });
145
+
146
+ assert.equal(result.caixa_maximo_negativo, -1000);
147
+ assert.equal(result.payback_meses, 63);
148
+ assert.equal(result.regras_aplicadas.fee_sobre_volume_reduzido_70, true);
149
+ });
150
+
151
+ test("deduz portabilidade e fee com parcela reduzida em 70% do volume", () => {
152
+ const result = calcularIndicadoresSecundario({
153
+ fluxos_mensais: [-1000, 500, 700],
154
+ transacoes_portabilidade: [
155
+ { valor: 100000, contemplada: true },
156
+ { valor: 100000, contemplada: false },
157
+ ],
158
+ volume_contratado: 300000,
159
+ fee_percentual: 0.02,
160
+ percentual_cartas_parcela_reduzida: 1,
161
+ });
162
+
163
+ assert.equal(result.custo_portabilidade, 1500);
164
+ assert.equal(result.custo_fee_parcela_reduzida, 4200);
165
+ assert.equal(result.custo_total_deducoes, 5700);
166
+ assert.equal(result.fluxo_ajustado[0], -6700);
167
+ });
168
+
169
+ test("valida entradas invalidas no secundario", () => {
170
+ assert.throws(() => {
171
+ calcularIndicadoresSecundario({
172
+ fluxos_mensais: [-100, 100],
173
+ percentual_cartas_parcela_reduzida: 1.2,
174
+ });
175
+ }, /percentual_cartas_parcela_reduzida/);
176
+ });
177
+
178
+ test("calcula tir mensal e anual no secundario", () => {
179
+ const result = calcularIndicadoresSecundario({
180
+ fluxos_mensais: [-1000, 1100],
181
+ });
182
+
183
+ assert.ok(result.tir_mensal !== null);
184
+ assert.ok(result.tir_anual !== null);
185
+ assert.ok(Math.abs(result.tir_mensal - 0.1) < 1e-6);
186
+ });
187
+
188
+ test("lista perfis SCP default com clientes entregues", () => {
189
+ const catalogo = listarPerfisFluxoScpDefault();
190
+ assert.ok(catalogo.clients.length >= 6);
191
+ assert.ok(catalogo.patterns.length >= 2);
192
+ assert.ok(catalogo.clients.find((c) => c.client_id === "extrato_investidor_6"));
193
+ });
194
+
195
+ test("monta fluxo SCP parametrizado por escala e deslocamento", () => {
196
+ const fluxo = montarFluxoClienteScp({
197
+ client_id: "extrato_investidor_6",
198
+ escala_fluxo: 0.5,
199
+ deslocamento_meses: 2,
200
+ novo_client_id: "cliente_novo",
201
+ novo_client_name: "Cliente Novo",
202
+ });
203
+
204
+ assert.equal(fluxo.client_id, "cliente_novo");
205
+ assert.equal(fluxo.client_name, "Cliente Novo");
206
+ assert.equal(fluxo.source_client_id, "extrato_investidor_6");
207
+ assert.ok(fluxo.pontos.length > 10);
208
+ assert.equal(fluxo.pontos[0].periodo, "2021-04-01");
209
+ });
210
+
211
+ test("simula secundario com perfil SCP default", () => {
212
+ const result = simularSecundarioComPerfilScp({
213
+ client_id: "extrato_investidor_6",
214
+ fee_percentual: 0.02,
215
+ volume_contratado: 100000,
216
+ percentual_cartas_parcela_reduzida: 0.5,
217
+ });
218
+
219
+ assert.equal(result.fluxo_cliente.client_id, "extrato_investidor_6");
220
+ assert.ok(result.fluxo_ajustado.length > 10);
221
+ assert.ok(typeof result.caixa_maximo_negativo === "number");
222
+ });
223
+
224
+ test("simula modo conversao fechado SCP -> imovel + secundario", () => {
225
+ const result = simularModoConversaoScpConsorcio({
226
+ client_id: "extrato_investidor_6",
227
+ valor_imovel_base: 600000,
228
+ desconto_imovel_percentual: 0.15,
229
+ percentual_entrada_imovel: 0.25,
230
+ agio_consorcio_percentual: 0.1,
231
+ taxa_custos_transacao_percentual: 0.02,
232
+ fee_percentual: 0.02,
233
+ volume_contratado: 120000,
234
+ percentual_cartas_parcela_reduzida: 0.5,
235
+ });
236
+
237
+ assert.ok(result.modo_conversao);
238
+ assert.equal(result.modo_conversao.valor_imovel_com_desconto, 510000);
239
+ assert.equal(result.modo_conversao.valor_entrada_imovel, 127500);
240
+ assert.ok(typeof result.modo_conversao.lucro_projetado_conversao === "number");
241
+ });
242
+
243
+ test("carrega fixtures de paridade Rodobens no pacote do motor", () => {
244
+ assert.equal(rodobensParityFixtures.fixture_version, "1.0");
245
+ assert.equal(rodobensParityFixtures.generated_from, "SIMULADOR RODOBENS - BTG 5.xlsm");
246
+ assert.equal(rodobensParityFixtures.scenarios.length, 3);
247
+
248
+ const plans = rodobensParityFixtures.scenarios.map((scenario) => scenario.plan).sort();
249
+ assert.deepEqual(plans, ["degrau", "linear", "pontual"]);
250
+ });
251
+
252
+ test("compara outputs Rodobens contra o oracle do workbook por campo", () => {
253
+ for (const scenario of rodobensParityFixtures.scenarios) {
254
+ const result = calcularParidadeRodobens(buildRodobensParityInputFromScenario(scenario));
255
+ assertParityOutputs(result, scenario.expected_outputs);
256
+ }
257
+ });
258
+
259
+ test("expõe warnings e saneamento do Pontual para a suite de paridade", () => {
260
+ const pontualScenario = rodobensParityFixtures.scenarios.find(
261
+ (scenario) => scenario.plan === "pontual",
262
+ );
263
+
264
+ assert.ok(pontualScenario);
265
+ assert.ok(
266
+ pontualScenario.warnings.some((warning) =>
267
+ /Automóvel|Automovel|Imóvel|Imovel/.test(warning),
268
+ ),
269
+ );
270
+
271
+ assert.equal(rodobensPontualImovelSanitized.plan, "pontual_imovel_sanitized");
272
+ assert.ok(rodobensPontualImovelSanitized.sanitized_fields.length >= 5);
273
+ assert.ok(rodobensPontualImovelSanitized.legacy_excluded.length >= 3);
274
+ });