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

chore: deploy web_comercial from monorepo

Browse files
README.md CHANGED
@@ -5,7 +5,7 @@ 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
@@ -22,9 +22,9 @@ Este é o ambiente de implantação otimizado do **Simulador de Consórcio**, pa
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**.
 
5
  colorTo: indigo
6
  sdk: docker
7
  pinned: true
8
+ hardware: cpu-basic
9
  ---
10
 
11
  ## MAIA v2 - Simulador de Consórcios Corporativo
 
22
 
23
  Este Space foi arquitetado sob governança estrita para uso máximo de **Assinatura Hugging Face PRO**:
24
 
25
+ 1. **Hardware Dedicado (CPU/Upgrades):**
26
+ Como Spaces em SDK Docker não suportam as instâncias de GPU fracionada (ZeroGPU - restritas ao Gradio), a arquitetura provisiona instâncias _CPU-Basic_ ou submete upgrades (_cpu-upgrade_) por demanda corporativa.
27
+ O utilitário interno Python (`DeviceManager`) é agnóstico: ele rebaixa graciosamente a execução da IA para instâncias CPU rodando em Nuvem, ou usa MPS (Apple Silicon) nativamente quando você testa local. **Nunca** altere bibliotecas obrigando a execução sob CUDA.
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**.
apps/web_comercial/next.config.ts CHANGED
@@ -1,9 +1,7 @@
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
  },
 
1
  import type { NextConfig } from "next";
 
2
 
3
  const nextConfig: NextConfig = {
4
  transpilePackages: ["@simulador-consorcio/simulation-engine"],
 
5
  experimental: {
6
  externalDir: true,
7
  },
apps/web_comercial/src/app/globals.css CHANGED
@@ -1,259 +1,26 @@
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
- }
 
1
  @import "tailwindcss";
2
 
3
  :root {
4
+ --background: #ffffff;
5
+ --foreground: #171717;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  }
7
 
8
  @theme inline {
9
  --color-background: var(--background);
10
  --color-foreground: var(--foreground);
11
+ --font-sans: "Inter", "Segoe UI", "Roboto", "Helvetica Neue", Arial, sans-serif;
12
+ --font-mono: "SFMono-Regular", "Menlo", "Monaco", "Consolas", "Liberation Mono", monospace;
13
  }
14
 
15
+ @media (prefers-color-scheme: dark) {
16
+ :root {
17
+ --background: #0a0a0a;
18
+ --foreground: #ededed;
19
+ }
 
 
20
  }
21
 
22
  body {
23
+ background: var(--background);
24
+ color: var(--foreground);
 
 
 
 
25
  font-family: var(--font-sans);
26
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
apps/web_comercial/src/app/layout.tsx CHANGED
@@ -1,14 +1,5 @@
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",
@@ -22,9 +13,7 @@ export default function RootLayout({
22
  }>) {
23
  return (
24
  <html lang="pt-BR">
25
- <body className={`${publicSans.className} antialiased`}>
26
- <AppLayout>{children}</AppLayout>
27
- </body>
28
  </html>
29
  );
30
  }
 
1
  import type { Metadata } from "next";
 
 
2
  import "./globals.css";
 
 
 
 
 
 
 
3
 
4
  export const metadata: Metadata = {
5
  title: "Apex Consorcios | Simulador",
 
13
  }>) {
14
  return (
15
  <html lang="pt-BR">
16
+ <body className="antialiased">{children}</body>
 
 
17
  </html>
18
  );
19
  }
apps/web_comercial/src/app/mercado-secundario/page.tsx CHANGED
@@ -155,47 +155,6 @@ export default function MercadoSecundarioPage() {
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" }}
 
155
  </div>
156
  </div>
157
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  <div
159
  className="module-content"
160
  style={{ display: "flex", flexDirection: "column", gap: "24px" }}
apps/web_comercial/src/app/page.tsx CHANGED
@@ -1,57 +1,75 @@
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
  }
 
1
  import { SimulatorForm } from "@/components/simulator/SimulatorForm";
 
2
 
3
  export default function Home() {
4
  return (
5
+ <div className="min-h-screen bg-black text-white selection:bg-blue-500/30">
6
+ <div className="absolute inset-0 z-0 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-blue-900/20 via-black to-black"></div>
 
 
 
 
 
 
7
 
8
+ <main className="relative z-10 flex flex-col items-center min-h-screen p-8 sm:p-20 font-sans">
9
+ <header className="w-full max-w-6xl flex justify-between items-center mb-16 animate-in slide-in-from-top-8 duration-700">
10
+ <div className="flex items-center gap-3">
11
+ <div className="w-10 h-10 bg-gradient-to-tr from-blue-600 to-indigo-500 rounded-xl flex items-center justify-center shadow-lg shadow-blue-500/20">
12
+ <svg
13
+ className="w-6 h-6 text-white"
14
+ fill="none"
15
+ stroke="currentColor"
16
+ viewBox="0 0 24 24"
17
+ >
18
+ <path
19
+ strokeLinecap="round"
20
+ strokeLinejoin="round"
21
+ strokeWidth="2"
22
+ d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
23
+ ></path>
24
+ </svg>
25
+ </div>
26
+ <h1 className="text-xl font-bold tracking-tight text-transparent bg-clip-text bg-gradient-to-r from-white to-gray-400">
27
+ Apex Consórcios
28
+ </h1>
29
+ </div>
30
+ <nav className="hidden md:flex gap-6 text-sm font-medium text-gray-400">
31
+ <a
32
+ href="#"
33
+ className="text-white hover:text-blue-400 transition-colors"
34
+ >
35
+ Simulador
36
+ </a>
37
+ <a href="#" className="hover:text-blue-400 transition-colors">
38
+ Mercado Secundário
39
+ </a>
40
+ <a href="#" className="hover:text-blue-400 transition-colors">
41
+ Configurações
42
+ </a>
43
+ </nav>
44
+ </header>
45
 
46
+ <section className="w-full flex-grow flex flex-col items-center mt-8">
47
+ <div className="text-center max-w-3xl mb-16 animate-in zoom-in-95 duration-700 delay-150">
48
+ <h2 className="text-5xl md:text-6xl font-extrabold tracking-tighter mb-6 leading-tight">
49
+ Simule o futuro do seu{" "}
50
+ <span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-indigo-500">
51
+ patrimônio
52
+ </span>
53
+ </h2>
54
+ <p className="text-lg md:text-xl text-gray-400 max-w-2xl mx-auto leading-relaxed">
55
+ O modelo determinístico oficial da Rodobens parametrizado para
56
+ viabilidade primária e revenda secundária.
57
+ </p>
58
+ </div>
59
 
60
+ <SimulatorForm />
61
+ </section>
62
+
63
+ <footer className="w-full max-w-6xl mt-24 pt-8 border-t border-white/10 text-center text-sm text-gray-500 flex justify-between items-center">
64
+ <p>© 2026 Apex Consórcios. Motor Baseado em Regras Rodobens.</p>
65
+ <div className="flex gap-4">
66
+ <span className="flex items-center gap-1.5">
67
+ <div className="w-2 h-2 rounded-full bg-emerald-500"></div> API
68
+ Determinística Ativa
69
+ </span>
70
+ </div>
71
+ </footer>
72
+ </main>
73
  </div>
74
  );
75
  }
apps/web_comercial/src/components/simulator/SimulatorForm.tsx CHANGED
@@ -55,53 +55,25 @@ export function SimulatorForm() {
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}
@@ -129,7 +101,13 @@ export function SimulatorForm() {
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"
@@ -160,15 +138,24 @@ export function SimulatorForm() {
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 && (
@@ -205,7 +192,14 @@ export function SimulatorForm() {
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
@@ -214,10 +208,20 @@ export function SimulatorForm() {
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)}
@@ -247,16 +251,24 @@ export function SimulatorForm() {
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%"
@@ -265,25 +277,19 @@ export function SimulatorForm() {
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>
@@ -342,10 +348,6 @@ export function SimulatorForm() {
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>
 
55
  setResultado(res);
56
  };
57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  return (
59
+ <div style={{ display: "flex", gap: "24px", alignItems: "flex-start" }}>
60
+ {/* Coluna Esquerda: Entrada de Dados */}
61
+ <div
62
+ style={{
63
+ flex: 2,
64
+ display: "flex",
65
+ flexDirection: "column",
66
+ gap: "24px",
67
+ }}
68
+ >
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  <Card title="Informações Básicas">
70
+ <div
71
+ style={{
72
+ display: "grid",
73
+ gridTemplateColumns: "1fr 1fr",
74
+ gap: "16px",
75
+ }}
76
+ >
77
  <Select
78
  label="Tipo Produto"
79
  value={produto}
 
101
  </Card>
102
 
103
  <Card title="Informações do Plano">
104
+ <div
105
+ style={{
106
+ display: "grid",
107
+ gridTemplateColumns: "1fr 1fr",
108
+ gap: "16px",
109
+ }}
110
+ >
111
  <Input
112
  label="Valor do Crédito"
113
  type="number"
 
138
  </Card>
139
 
140
  <Card title="Informações de Lance">
141
+ <div
142
+ style={{
143
+ display: "grid",
144
+ gridTemplateColumns: "1fr 1fr",
145
+ gap: "16px",
146
+ }}
147
+ >
148
  <div className="ui-form-group" style={{ gridColumn: "1 / -1" }}>
149
+ <label
150
+ className="ui-form-label"
151
+ style={{ display: "flex", alignItems: "center", gap: "8px" }}
152
+ >
153
  <input
154
  type="checkbox"
155
  checked={temLance}
156
  onChange={(e) => setTemLance(e.target.checked)}
157
  />
158
+ Simular com Lance
159
  </label>
160
  </div>
161
  {temLance && (
 
192
  )}
193
  </div>
194
 
195
+ <div
196
+ style={{
197
+ marginTop: "24px",
198
+ display: "flex",
199
+ justifyContent: "flex-end",
200
+ gap: "12px",
201
+ }}
202
+ >
203
  <button className="btn btn-secondary">Limpar</button>
204
  <button className="btn btn-primary" onClick={handleSimular}>
205
  Calcular Cenário
 
208
  </Card>
209
  </div>
210
 
211
+ {/* Coluna Direita: Resumos e Análise */}
212
+ <div
213
+ style={{
214
+ flex: 1,
215
+ display: "flex",
216
+ flexDirection: "column",
217
+ gap: "24px",
218
+ }}
219
+ >
220
  <Card title="Resumo Geral" highlight={true}>
221
  {resultado ? (
222
+ <div
223
+ style={{ display: "flex", flexDirection: "column", gap: "12px" }}
224
+ >
225
  <DataCard
226
  label="Crédito Disponível"
227
  value={formatCurrency(resultado.credito || credito)}
 
251
  )}
252
  </div>
253
  ) : (
254
+ <div
255
+ style={{
256
+ padding: "32px 16px",
257
+ textAlign: "center",
258
+ color: "var(--text-muted)",
259
+ fontSize: "0.9rem",
260
+ }}
261
+ >
262
+ Preencha os dados e clique em Calcular para visualizar o resumo.
263
+ </div>
264
  )}
265
  </Card>
266
 
267
  <Card title="CET / Ao mês">
268
  {resultado ? (
269
+ <div
270
+ style={{ display: "flex", flexDirection: "column", gap: "12px" }}
271
+ >
272
  <DataCard
273
  label="Custo Efetivo Total (Ao Ano)"
274
  value="11.45%"
 
277
  <DataCard label="Taxa Equivalente (Ao Mês)" value="0.91%" />
278
  </div>
279
  ) : (
280
+ <div
281
+ style={{
282
+ padding: "32px 16px",
283
+ textAlign: "center",
284
+ color: "var(--text-muted)",
285
+ }}
286
+ >
287
+ Aguardando simulação.
 
 
 
288
  </div>
289
+ )}
 
 
 
 
290
  </Card>
291
 
292
+ {/* Informações da Parcela Table Demo */}
293
  {resultado && (
294
  <Card title="Composição da Parcela" noPadding>
295
  <Table>
 
348
  </TableRow>
349
  </TableBody>
350
  </Table>
 
 
 
 
351
  </Card>
352
  )}
353
  </div>
apps/web_comercial/src/components/ui/Card.tsx CHANGED
@@ -1,4 +1,5 @@
1
  import React from "react";
 
2
 
3
  interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
4
  children: React.ReactNode;
 
1
  import React from "react";
2
+ import "./ui.css";
3
 
4
  interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
5
  children: React.ReactNode;
apps/web_comercial/src/components/ui/ui.css CHANGED
@@ -1,44 +1,38 @@
 
 
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
 
@@ -46,79 +40,73 @@
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
 
@@ -131,31 +119,23 @@
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 {
@@ -166,7 +146,7 @@
166
  .ui-input-suffix {
167
  position: absolute;
168
  color: var(--text-muted);
169
- font-size: 0.86rem;
170
  }
171
 
172
  .ui-input-prefix {
@@ -178,17 +158,17 @@
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 */
@@ -203,18 +183,17 @@
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 {
@@ -225,7 +204,7 @@
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 {
@@ -237,43 +216,43 @@
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 */
@@ -289,19 +268,17 @@
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 {
@@ -309,8 +286,9 @@
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 {
 
1
+ /* UI Components CSS */
2
+
3
  /* Card */
4
  .ui-card {
5
  background-color: var(--bg-panel);
6
  border: 1px solid var(--border-color);
7
+ border-radius: 6px;
8
  overflow: hidden;
9
  display: flex;
10
  flex-direction: column;
 
11
  }
12
 
13
  .ui-card.highlight {
14
+ border-color: #bfd4f5;
15
+ box-shadow: 0 0 0 1px rgba(0, 82, 204, 0.08);
 
 
16
  }
17
 
18
  .ui-card-header {
19
+ padding: 16px 20px;
 
20
  border-bottom: 1px solid var(--border-color);
21
  display: flex;
22
  justify-content: space-between;
23
  align-items: center;
24
+ background-color: #fafbfc;
 
 
25
  }
26
 
27
  .ui-card-title {
28
+ font-size: 0.95rem;
29
+ font-weight: 600;
30
  color: var(--color-brand-dark);
31
  margin: 0;
 
 
32
  }
33
 
34
  .ui-card-content {
35
+ padding: 20px;
36
  flex: 1;
37
  }
38
 
 
40
  padding: 0;
41
  }
42
 
43
+ /* Data Card (Tabular display style) */
44
  .ui-data-card {
45
  display: flex;
46
  flex-direction: column;
47
+ padding: 12px 16px;
 
48
  border: 1px solid var(--border-color);
49
+ border-radius: 4px;
50
+ background-color: #fafbfc;
 
 
51
  }
52
 
53
  .ui-data-card.highlight {
54
+ background-color: var(--bg-panel-light-blue);
55
+ border-color: #bfdbfe;
 
 
56
  }
57
 
58
  .ui-data-label {
59
+ font-size: 0.75rem;
60
+ font-weight: 600;
61
  color: var(--text-muted);
62
  text-transform: uppercase;
63
+ letter-spacing: 0.5px;
64
+ margin-bottom: 4px;
65
  }
66
 
67
  .ui-data-value {
68
+ font-size: 1.25rem;
69
  font-weight: 700;
70
  color: var(--text-main);
 
71
  }
72
 
73
  .ui-data-subvalue {
74
  font-size: 0.75rem;
75
  color: var(--text-muted);
76
+ margin-top: 2px;
77
  }
78
 
79
  /* Form Controls */
80
  .ui-form-group {
81
+ margin-bottom: 16px;
82
  display: flex;
83
  flex-direction: column;
 
84
  }
85
 
86
  .ui-label-wrapper {
87
  display: flex;
88
  align-items: center;
89
  gap: 6px;
90
+ margin-bottom: 6px;
91
  }
92
 
93
  .ui-form-label {
94
+ font-size: 0.85rem;
95
+ font-weight: 600;
96
+ color: var(--text-main);
 
 
97
  }
98
 
99
  .ui-tooltip-icon {
100
  display: inline-flex;
101
  align-items: center;
102
  justify-content: center;
103
+ width: 14px;
104
+ height: 14px;
105
  border-radius: 50%;
106
+ background-color: var(--border-focus);
107
  color: white;
108
  font-size: 10px;
109
+ font-weight: bold;
110
  cursor: help;
111
  }
112
 
 
119
  .ui-input,
120
  .ui-select {
121
  width: 100%;
122
+ padding: 8px 12px;
123
+ font-size: 0.9rem;
 
124
  border: 1px solid var(--border-color);
125
+ border-radius: 4px;
126
+ background-color: white;
127
  color: var(--text-main);
128
  transition:
129
+ border-color 0.2s,
130
+ box-shadow 0.2s;
 
131
  font-family: inherit;
132
  }
133
 
 
 
 
 
 
134
  .ui-input:focus,
135
  .ui-select:focus {
136
  outline: none;
137
+ border-color: var(--color-brand-accent);
138
+ box-shadow: 0 0 0 2px rgba(0, 82, 204, 0.1);
 
139
  }
140
 
141
  .ui-input.is-invalid {
 
146
  .ui-input-suffix {
147
  position: absolute;
148
  color: var(--text-muted);
149
+ font-size: 0.9rem;
150
  }
151
 
152
  .ui-input-prefix {
 
158
  }
159
 
160
  .ui-input.has-prefix {
161
+ padding-left: 32px;
162
  }
163
 
164
  .ui-input.has-suffix {
165
+ padding-right: 36px;
166
  }
167
 
168
  .ui-form-error {
169
  color: var(--status-negative);
170
  font-size: 0.75rem;
171
+ margin-top: 4px;
172
  }
173
 
174
  /* Tooltip */
 
183
  bottom: 150%;
184
  left: 50%;
185
  transform: translateX(-50%);
186
+ background-color: #1e293b;
187
  color: white;
188
  text-align: center;
189
+ padding: 6px 10px;
190
+ border-radius: 4px;
191
+ font-size: 0.75rem;
192
  white-space: nowrap;
193
  opacity: 0;
194
  transition: opacity 0.2s;
195
  z-index: 100;
196
  pointer-events: none;
 
197
  }
198
 
199
  .ui-tooltip-content::after {
 
204
  margin-left: -5px;
205
  border-width: 5px;
206
  border-style: solid;
207
+ border-color: #1e293b transparent transparent transparent;
208
  }
209
 
210
  .ui-tooltip-container:hover .ui-tooltip-content {
 
216
  .ui-table-container {
217
  width: 100%;
218
  overflow-x: auto;
219
+ border-radius: 4px;
220
  }
221
 
222
  .ui-table {
223
  width: 100%;
224
  border-collapse: collapse;
225
+ font-size: 0.85rem;
226
  text-align: left;
227
  }
228
 
229
  .ui-table-head {
230
+ background-color: var(--bg-main);
231
+ border-bottom: 2px solid var(--border-color);
232
  }
233
 
234
  .ui-table-header {
235
  padding: 12px 16px;
236
+ font-weight: 600;
237
  color: var(--text-muted);
238
  text-transform: uppercase;
239
+ font-size: 0.75rem;
240
+ letter-spacing: 0.5px;
241
  white-space: nowrap;
242
  }
243
 
244
  .ui-table-row {
245
+ border-bottom: 1px solid var(--border-color);
246
+ transition: background-color 0.2s;
247
  }
248
 
249
  .ui-table-row:hover {
250
+ background-color: var(--bg-main);
251
  }
252
 
253
  .ui-table-cell {
254
  padding: 12px 16px;
255
  color: var(--text-main);
 
256
  }
257
 
258
  /* Tabs */
 
268
  }
269
 
270
  .ui-tab-button {
271
+ padding: 12px 24px;
272
  background: none;
273
  border: none;
274
  border-bottom: 2px solid transparent;
275
+ font-size: 0.9rem;
276
+ font-weight: 500;
277
  color: var(--text-muted);
278
  cursor: pointer;
279
+ transition: all 0.2s;
280
  font-family: inherit;
281
+ margin-bottom: -1px; /* Overlap border */
 
 
282
  }
283
 
284
  .ui-tab-button:hover {
 
286
  }
287
 
288
  .ui-tab-button.active {
289
+ color: var(--color-brand-accent);
290
  border-bottom-color: var(--color-brand-accent);
291
+ font-weight: 600;
292
  }
293
 
294
  .ui-tabs-content {
packages/simulation-engine/src/index.d.ts CHANGED
@@ -31,30 +31,6 @@ export interface ResultadoSimulacao {
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;
@@ -175,9 +151,6 @@ export interface ResultadoSimulacaoConversaoScpConsorcio
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;
 
31
  prazo_apos_lance?: number;
32
  }
33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  export interface TransacaoPortabilidade {
35
  valor: number;
36
  contemplada: boolean;
 
151
  }
152
 
153
  export declare function calcularSimulacao(input: SimulacaoInput): ResultadoSimulacao;
 
 
 
154
  export declare function calcularIndicadoresSecundario(
155
  input: SimulacaoSecundarioInput
156
  ): ResultadoSimulacaoSecundario;
packages/simulation-engine/src/index.js CHANGED
@@ -1,5 +1,4 @@
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) {
@@ -36,40 +35,6 @@ function ensureInteger(name, value) {
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));
@@ -252,249 +217,6 @@ export function calcularSimulacao(input) {
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) {
 
1
  import { SCP_CLIENTS_DEFAULT, SCP_FLOW_PATTERNS_DEFAULT } from "./scpDefaults.js";
 
2
 
3
  function ensurePositiveNumber(name, value) {
4
  if (typeof value !== "number" || Number.isNaN(value) || value <= 0) {
 
35
  }
36
  }
37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  function addMonths(isoDate, deltaMonths) {
39
  const [year, month, day] = isoDate.split("-").map(Number);
40
  const base = new Date(Date.UTC(year, month - 1, day || 1));
 
217
  };
218
  }
219
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  function npv(rate, cashflows) {
221
  let value = 0;
222
  for (let i = 0; i < cashflows.length; i += 1) {
packages/simulation-engine/tests/simulation-engine.test.js CHANGED
@@ -1,8 +1,6 @@
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,
@@ -11,23 +9,6 @@ import {
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: {
@@ -41,60 +22,6 @@ const baseInput = {
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
 
@@ -239,36 +166,3 @@ test("simula modo conversao fechado SCP -> imovel + secundario", () => {
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
- });
 
1
  import test from "node:test";
2
  import assert from "node:assert/strict";
 
3
  import {
 
4
  calcularIndicadoresSecundario,
5
  calcularSimulacao,
6
  listarPerfisFluxoScpDefault,
 
9
  simularSecundarioComPerfilScp,
10
  } from "../src/index.js";
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  const baseInput = {
13
  credito_desejado: 300000,
14
  parametros: {
 
22
  tem_lance: false,
23
  };
24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  test("calcula parcela integral com valores esperados", () => {
26
  const result = calcularSimulacao(baseInput);
27
 
 
166
  assert.equal(result.modo_conversao.valor_entrada_imovel, 127500);
167
  assert.ok(typeof result.modo_conversao.lucro_projetado_conversao === "number");
168
  });