Spaces:
Sleeping
Sleeping
chore: deploy web_comercial from monorepo
Browse files- .dockerignore +3 -0
- Dockerfile +20 -0
- README.md +3 -3
- apps/web_comercial/.gitignore +41 -0
- apps/web_comercial/README.md +16 -0
- apps/web_comercial/eslint.config.mjs +18 -0
- apps/web_comercial/next.config.ts +10 -0
- apps/web_comercial/package-lock.json +0 -0
- apps/web_comercial/package.json +27 -0
- apps/web_comercial/postcss.config.mjs +7 -0
- apps/web_comercial/public/file.svg +1 -0
- apps/web_comercial/public/globe.svg +1 -0
- apps/web_comercial/public/next.svg +1 -0
- apps/web_comercial/public/vercel.svg +1 -0
- apps/web_comercial/public/window.svg +1 -0
- apps/web_comercial/src/app/favicon.ico +0 -0
- apps/web_comercial/src/app/globals.css +26 -0
- apps/web_comercial/src/app/layout.tsx +19 -0
- apps/web_comercial/src/app/page.tsx +75 -0
- apps/web_comercial/src/components/simulator/SimulatorForm.tsx +248 -0
- apps/web_comercial/tsconfig.json +42 -0
- packages/simulation-engine/package.json +14 -0
- packages/simulation-engine/src/index.d.ts +34 -0
- packages/simulation-engine/src/index.js +95 -0
- packages/simulation-engine/tests/simulation-engine.test.js +50 -0
.dockerignore
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
**/.next
|
| 2 |
+
**/node_modules
|
| 3 |
+
**/.DS_Store
|
Dockerfile
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:22-alpine 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-alpine AS runner
|
| 11 |
+
WORKDIR /workspace
|
| 12 |
+
ENV NODE_ENV=production
|
| 13 |
+
ENV PORT=7860
|
| 14 |
+
ENV HOSTNAME=0.0.0.0
|
| 15 |
+
|
| 16 |
+
COPY --from=builder /workspace/apps/web_comercial ./apps/web_comercial
|
| 17 |
+
COPY --from=builder /workspace/packages/simulation-engine ./packages/simulation-engine
|
| 18 |
+
|
| 19 |
+
EXPOSE 7860
|
| 20 |
+
CMD ["sh", "-c", "cd apps/web_comercial && npm run start -- -H 0.0.0.0 -p 7860"]
|
README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: indigo
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Simulador Consorcio
|
| 3 |
+
aemoji: chart_with_upwards_trend
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: indigo
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
+
Docker Space para executar `apps/web_comercial` com o pacote interno `packages/simulation-engine`.
|
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.config.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { NextConfig } from "next";
|
| 2 |
+
|
| 3 |
+
const nextConfig: NextConfig = {
|
| 4 |
+
transpilePackages: ["@simulador-consorcio/simulation-engine"],
|
| 5 |
+
experimental: {
|
| 6 |
+
externalDir: true,
|
| 7 |
+
},
|
| 8 |
+
};
|
| 9 |
+
|
| 10 |
+
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/favicon.ico
ADDED
|
|
apps/web_comercial/src/app/globals.css
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Metadata } from "next";
|
| 2 |
+
import "./globals.css";
|
| 3 |
+
|
| 4 |
+
export const metadata: Metadata = {
|
| 5 |
+
title: "Apex Consorcios | Simulador",
|
| 6 |
+
description: "Simulador deterministico de consorcio imobiliario",
|
| 7 |
+
};
|
| 8 |
+
|
| 9 |
+
export default function RootLayout({
|
| 10 |
+
children,
|
| 11 |
+
}: Readonly<{
|
| 12 |
+
children: React.ReactNode;
|
| 13 |
+
}>) {
|
| 14 |
+
return (
|
| 15 |
+
<html lang="pt-BR">
|
| 16 |
+
<body className="antialiased">{children}</body>
|
| 17 |
+
</html>
|
| 18 |
+
);
|
| 19 |
+
}
|
apps/web_comercial/src/app/page.tsx
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { useState } from "react";
|
| 4 |
+
import { calcularSimulacao } from "@simulador-consorcio/simulation-engine";
|
| 5 |
+
import {
|
| 6 |
+
type ParametrosPlano,
|
| 7 |
+
type ResultadoSimulacao,
|
| 8 |
+
type SimulacaoInput,
|
| 9 |
+
} from "@simulador-consorcio/simulation-engine";
|
| 10 |
+
|
| 11 |
+
export function SimulatorForm() {
|
| 12 |
+
const [credito, setCredito] = useState<number>(300000);
|
| 13 |
+
const [prazo, setPrazo] = useState<number>(180);
|
| 14 |
+
const [temLance, setTemLance] = useState<boolean>(false);
|
| 15 |
+
const [lancePercentual, setLancePercentual] = useState<number>(30);
|
| 16 |
+
const [resultado, setResultado] = useState<ResultadoSimulacao | null>(null);
|
| 17 |
+
|
| 18 |
+
const formatCurrency = (value: number) => {
|
| 19 |
+
return new Intl.NumberFormat("pt-BR", {
|
| 20 |
+
style: "currency",
|
| 21 |
+
currency: "BRL",
|
| 22 |
+
}).format(value);
|
| 23 |
+
};
|
| 24 |
+
|
| 25 |
+
const handleSimular = () => {
|
| 26 |
+
const parametros: ParametrosPlano = {
|
| 27 |
+
id_plano: "PADRAO",
|
| 28 |
+
nome_plano: "Plano Padrão Imóvel",
|
| 29 |
+
prazo_meses: prazo,
|
| 30 |
+
taxa_administracao_total: 0.15, // 15%
|
| 31 |
+
fundo_reserva_mensal: 0.0005, // 0.05%
|
| 32 |
+
seguro_prestamista: 0.035, // 3.5%
|
| 33 |
+
};
|
| 34 |
+
|
| 35 |
+
const input: SimulacaoInput = {
|
| 36 |
+
credito_desejado: credito,
|
| 37 |
+
parametros,
|
| 38 |
+
tem_lance: temLance,
|
| 39 |
+
lance_percentual: temLance ? lancePercentual / 100 : undefined,
|
| 40 |
+
};
|
| 41 |
+
|
| 42 |
+
const res = calcularSimulacao(input);
|
| 43 |
+
setResultado(res);
|
| 44 |
+
};
|
| 45 |
+
|
| 46 |
+
return (
|
| 47 |
+
<div className="flex flex-col md:flex-row gap-8 w-full max-w-6xl mx-auto p-4">
|
| 48 |
+
{/* Formulário */}
|
| 49 |
+
<div className="w-full md:w-1/2 bg-white/5 backdrop-blur-xl border border-white/10 rounded-3xl p-8 shadow-2xl transition-all hover:shadow-[0_0_40px_rgba(59,130,246,0.15)]">
|
| 50 |
+
<h2 className="text-2xl font-semibold mb-6 text-white tracking-tight">
|
| 51 |
+
Novo Cenário
|
| 52 |
+
</h2>
|
| 53 |
+
|
| 54 |
+
<div className="space-y-6">
|
| 55 |
+
<div>
|
| 56 |
+
<label className="block text-sm font-medium text-gray-300 mb-2">
|
| 57 |
+
Crédito Desejado (R$)
|
| 58 |
+
</label>
|
| 59 |
+
<input
|
| 60 |
+
type="number"
|
| 61 |
+
value={credito}
|
| 62 |
+
onChange={(e) => setCredito(Number(e.target.value))}
|
| 63 |
+
step="10000"
|
| 64 |
+
className="w-full bg-black/20 border border-gray-700 text-white rounded-xl px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all font-mono"
|
| 65 |
+
/>
|
| 66 |
+
</div>
|
| 67 |
+
|
| 68 |
+
<div>
|
| 69 |
+
<label className="block text-sm font-medium text-gray-300 mb-2">
|
| 70 |
+
Prazo (Meses)
|
| 71 |
+
</label>
|
| 72 |
+
<select
|
| 73 |
+
value={prazo}
|
| 74 |
+
onChange={(e) => setPrazo(Number(e.target.value))}
|
| 75 |
+
className="w-full bg-black/20 border border-gray-700 text-white rounded-xl px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500 appearance-none transition-all"
|
| 76 |
+
>
|
| 77 |
+
<option value="120">120 meses</option>
|
| 78 |
+
<option value="150">150 meses</option>
|
| 79 |
+
<option value="180">180 meses</option>
|
| 80 |
+
<option value="216">216 meses</option>
|
| 81 |
+
</select>
|
| 82 |
+
</div>
|
| 83 |
+
|
| 84 |
+
<div className="flex items-center space-x-3 pt-2">
|
| 85 |
+
<button
|
| 86 |
+
type="button"
|
| 87 |
+
onClick={() => setTemLance(!temLance)}
|
| 88 |
+
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${temLance ? "bg-blue-600" : "bg-gray-600"}`}
|
| 89 |
+
>
|
| 90 |
+
<span
|
| 91 |
+
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${temLance ? "translate-x-6" : "translate-x-1"}`}
|
| 92 |
+
/>
|
| 93 |
+
</button>
|
| 94 |
+
<label
|
| 95 |
+
className="text-sm font-medium text-gray-300 cursor-pointer"
|
| 96 |
+
onClick={() => setTemLance(!temLance)}
|
| 97 |
+
>
|
| 98 |
+
Incluir Lance
|
| 99 |
+
</label>
|
| 100 |
+
</div>
|
| 101 |
+
|
| 102 |
+
{temLance && (
|
| 103 |
+
<div className="animate-in fade-in slide-in-from-top-4 duration-300">
|
| 104 |
+
<label className="block text-sm font-medium text-gray-300 mb-2">
|
| 105 |
+
Percentual de Lance (%)
|
| 106 |
+
</label>
|
| 107 |
+
<input
|
| 108 |
+
type="number"
|
| 109 |
+
value={lancePercentual}
|
| 110 |
+
onChange={(e) => setLancePercentual(Number(e.target.value))}
|
| 111 |
+
min="10"
|
| 112 |
+
max="80"
|
| 113 |
+
className="w-full bg-black/20 border border-gray-700 text-white rounded-xl px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all"
|
| 114 |
+
/>
|
| 115 |
+
</div>
|
| 116 |
+
)}
|
| 117 |
+
|
| 118 |
+
<button
|
| 119 |
+
onClick={handleSimular}
|
| 120 |
+
className="w-full mt-8 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 text-white font-medium py-4 px-6 rounded-xl shadow-[0_0_20px_rgba(79,70,229,0.3)] hover:shadow-[0_0_30px_rgba(79,70,229,0.5)] transition-all transform hover:-translate-y-0.5 active:translate-y-0"
|
| 121 |
+
>
|
| 122 |
+
Calcular Cenário
|
| 123 |
+
</button>
|
| 124 |
+
</div>
|
| 125 |
+
</div>
|
| 126 |
+
|
| 127 |
+
{/* Resultados */}
|
| 128 |
+
<div className="w-full md:w-1/2 flex flex-col gap-6">
|
| 129 |
+
{resultado ? (
|
| 130 |
+
<div className="animate-in fade-in zoom-in-95 duration-500 space-y-6">
|
| 131 |
+
<div className="bg-gradient-to-br from-indigo-900/40 to-blue-900/20 border border-blue-500/30 rounded-3xl p-8 shadow-xl backdrop-blur-md">
|
| 132 |
+
<h3 className="text-lg text-blue-300 font-medium mb-1">
|
| 133 |
+
Parcela Integral (100%)
|
| 134 |
+
</h3>
|
| 135 |
+
<div className="text-5xl font-bold text-white tracking-tighter mb-6">
|
| 136 |
+
{formatCurrency(resultado.parcela_integral.total)}
|
| 137 |
+
</div>
|
| 138 |
+
|
| 139 |
+
<div className="grid grid-cols-2 gap-4 text-sm">
|
| 140 |
+
<div className="bg-black/20 p-3 rounded-lg border border-white/5">
|
| 141 |
+
<div className="text-gray-400 mb-1">Fundo Comum</div>
|
| 142 |
+
<div className="text-white font-mono">
|
| 143 |
+
{formatCurrency(resultado.parcela_integral.fundo_comum)}
|
| 144 |
+
</div>
|
| 145 |
+
</div>
|
| 146 |
+
<div className="bg-black/20 p-3 rounded-lg border border-white/5">
|
| 147 |
+
<div className="text-gray-400 mb-1">Taxa Admin (TA)</div>
|
| 148 |
+
<div className="text-white font-mono">
|
| 149 |
+
{formatCurrency(
|
| 150 |
+
resultado.parcela_integral.taxa_administracao,
|
| 151 |
+
)}
|
| 152 |
+
</div>
|
| 153 |
+
</div>
|
| 154 |
+
<div className="bg-black/20 p-3 rounded-lg border border-white/5">
|
| 155 |
+
<div className="text-gray-400 mb-1">Seguro/Outros</div>
|
| 156 |
+
<div className="text-white font-mono">
|
| 157 |
+
{formatCurrency(
|
| 158 |
+
resultado.parcela_integral.seguro +
|
| 159 |
+
resultado.parcela_integral.fundo_reserva,
|
| 160 |
+
)}
|
| 161 |
+
</div>
|
| 162 |
+
</div>
|
| 163 |
+
<div className="bg-black/20 p-3 rounded-lg border border-white/5">
|
| 164 |
+
<div className="text-gray-400 mb-1">Prazo</div>
|
| 165 |
+
<div className="text-white font-mono">
|
| 166 |
+
{resultado.prazo} meses
|
| 167 |
+
</div>
|
| 168 |
+
</div>
|
| 169 |
+
</div>
|
| 170 |
+
</div>
|
| 171 |
+
|
| 172 |
+
{resultado.parcela_reduzida && (
|
| 173 |
+
<div className="bg-white/5 backdrop-blur-xl border border-white/10 rounded-3xl p-6 shadow-lg hover:bg-white/10 transition-colors">
|
| 174 |
+
<h3 className="text-sm text-gray-400 font-medium mb-1">
|
| 175 |
+
Parcela Reduzida (Ate a contemplacao)
|
| 176 |
+
</h3>
|
| 177 |
+
<div className="text-3xl font-bold text-emerald-400 tracking-tight">
|
| 178 |
+
{formatCurrency(resultado.parcela_reduzida.total)}
|
| 179 |
+
</div>
|
| 180 |
+
</div>
|
| 181 |
+
)}
|
| 182 |
+
|
| 183 |
+
{resultado.valor_lance && resultado.prazo_apos_lance && (
|
| 184 |
+
<div className="bg-gradient-to-r from-amber-900/30 to-orange-900/20 border border-orange-500/30 rounded-3xl p-6 shadow-lg backdrop-blur-md">
|
| 185 |
+
<h3 className="text-sm text-orange-300 font-medium mb-4 flex items-center gap-2">
|
| 186 |
+
<svg
|
| 187 |
+
className="w-5 h-5"
|
| 188 |
+
fill="none"
|
| 189 |
+
stroke="currentColor"
|
| 190 |
+
viewBox="0 0 24 24"
|
| 191 |
+
>
|
| 192 |
+
<path
|
| 193 |
+
strokeLinecap="round"
|
| 194 |
+
strokeLinejoin="round"
|
| 195 |
+
strokeWidth="2"
|
| 196 |
+
d="M13 10V3L4 14h7v7l9-11h-7z"
|
| 197 |
+
></path>
|
| 198 |
+
</svg>
|
| 199 |
+
Projeção com Lance
|
| 200 |
+
</h3>
|
| 201 |
+
<div className="grid grid-cols-2 gap-4">
|
| 202 |
+
<div>
|
| 203 |
+
<div className="text-gray-400 text-xs uppercase tracking-wider mb-1">
|
| 204 |
+
Valor do Lance
|
| 205 |
+
</div>
|
| 206 |
+
<div className="text-2xl font-semibold text-white">
|
| 207 |
+
{formatCurrency(resultado.valor_lance)}
|
| 208 |
+
</div>
|
| 209 |
+
</div>
|
| 210 |
+
<div>
|
| 211 |
+
<div className="text-gray-400 text-xs uppercase tracking-wider mb-1">
|
| 212 |
+
Prazo Reduzido
|
| 213 |
+
</div>
|
| 214 |
+
<div className="text-2xl font-semibold text-white">
|
| 215 |
+
{resultado.prazo_apos_lance} meses
|
| 216 |
+
</div>
|
| 217 |
+
</div>
|
| 218 |
+
</div>
|
| 219 |
+
</div>
|
| 220 |
+
)}
|
| 221 |
+
</div>
|
| 222 |
+
) : (
|
| 223 |
+
<div className="h-full w-full flex items-center justify-center border-2 border-dashed border-gray-700/50 rounded-3xl p-12 text-center text-gray-500">
|
| 224 |
+
<div className="max-w-xs">
|
| 225 |
+
<svg
|
| 226 |
+
className="w-12 h-12 mx-auto mb-4 opacity-50 text-gray-400"
|
| 227 |
+
fill="none"
|
| 228 |
+
viewBox="0 0 24 24"
|
| 229 |
+
stroke="currentColor"
|
| 230 |
+
>
|
| 231 |
+
<path
|
| 232 |
+
strokeLinecap="round"
|
| 233 |
+
strokeLinejoin="round"
|
| 234 |
+
strokeWidth="1"
|
| 235 |
+
d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
| 236 |
+
/>
|
| 237 |
+
</svg>
|
| 238 |
+
<p>
|
| 239 |
+
Ajuste os parâmetros e clique em "Calcular Cenario" para exibir
|
| 240 |
+
as projeções.
|
| 241 |
+
</p>
|
| 242 |
+
</div>
|
| 243 |
+
</div>
|
| 244 |
+
)}
|
| 245 |
+
</div>
|
| 246 |
+
</div>
|
| 247 |
+
);
|
| 248 |
+
}
|
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/index.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 declare function calcularSimulacao(input: SimulacaoInput): ResultadoSimulacao;
|
packages/simulation-engine/src/index.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
function ensurePositiveNumber(name, value) {
|
| 2 |
+
if (typeof value !== "number" || Number.isNaN(value) || value <= 0) {
|
| 3 |
+
throw new Error(`${name} deve ser um numero maior que zero`);
|
| 4 |
+
}
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
function ensureNonNegativeNumber(name, value) {
|
| 8 |
+
if (typeof value !== "number" || Number.isNaN(value) || value < 0) {
|
| 9 |
+
throw new Error(`${name} deve ser um numero nao negativo`);
|
| 10 |
+
}
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
function validateInput(input) {
|
| 14 |
+
if (!input || typeof input !== "object") {
|
| 15 |
+
throw new Error("input invalido");
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
ensurePositiveNumber("credito_desejado", input.credito_desejado);
|
| 19 |
+
|
| 20 |
+
if (!input.parametros || typeof input.parametros !== "object") {
|
| 21 |
+
throw new Error("parametros invalidos");
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
const p = input.parametros;
|
| 25 |
+
|
| 26 |
+
ensurePositiveNumber("prazo_meses", p.prazo_meses);
|
| 27 |
+
ensureNonNegativeNumber("taxa_administracao_total", p.taxa_administracao_total);
|
| 28 |
+
ensureNonNegativeNumber("fundo_reserva_mensal", p.fundo_reserva_mensal);
|
| 29 |
+
ensureNonNegativeNumber("seguro_prestamista", p.seguro_prestamista);
|
| 30 |
+
|
| 31 |
+
if (input.tem_lance && input.lance_percentual !== undefined) {
|
| 32 |
+
ensureNonNegativeNumber("lance_percentual", input.lance_percentual);
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
export function calcularSimulacao(input) {
|
| 37 |
+
validateInput(input);
|
| 38 |
+
|
| 39 |
+
const { credito_desejado, parametros, tem_lance, lance_percentual } = input;
|
| 40 |
+
const {
|
| 41 |
+
prazo_meses,
|
| 42 |
+
taxa_administracao_total,
|
| 43 |
+
fundo_reserva_mensal,
|
| 44 |
+
seguro_prestamista,
|
| 45 |
+
} = parametros;
|
| 46 |
+
|
| 47 |
+
const fcMensal = 1.0 / prazo_meses;
|
| 48 |
+
const taMensal = taxa_administracao_total / prazo_meses;
|
| 49 |
+
|
| 50 |
+
const valorFC = credito_desejado * fcMensal;
|
| 51 |
+
const valorTA = credito_desejado * taMensal;
|
| 52 |
+
const valorFR = credito_desejado * fundo_reserva_mensal;
|
| 53 |
+
|
| 54 |
+
const valorBaseSeguro =
|
| 55 |
+
credito_desejado + credito_desejado * taxa_administracao_total;
|
| 56 |
+
const taxaSeguroMensal = seguro_prestamista / prazo_meses;
|
| 57 |
+
const valorSeguro = valorBaseSeguro * taxaSeguroMensal;
|
| 58 |
+
|
| 59 |
+
const parcelaIntegral = {
|
| 60 |
+
fundo_comum: valorFC,
|
| 61 |
+
taxa_administracao: valorTA,
|
| 62 |
+
fundo_reserva: valorFR,
|
| 63 |
+
seguro: valorSeguro,
|
| 64 |
+
total: valorFC + valorTA + valorFR + valorSeguro,
|
| 65 |
+
};
|
| 66 |
+
|
| 67 |
+
const fatorReducao = 0.7;
|
| 68 |
+
const valorFCReduzido = valorFC * fatorReducao;
|
| 69 |
+
|
| 70 |
+
const parcelaReduzida = {
|
| 71 |
+
fundo_comum: valorFCReduzido,
|
| 72 |
+
taxa_administracao: valorTA,
|
| 73 |
+
fundo_reserva: valorFR,
|
| 74 |
+
seguro: valorSeguro,
|
| 75 |
+
total: valorFCReduzido + valorTA + valorFR + valorSeguro,
|
| 76 |
+
};
|
| 77 |
+
|
| 78 |
+
let valorLance = 0;
|
| 79 |
+
let prazoAposLance = prazo_meses;
|
| 80 |
+
|
| 81 |
+
if (tem_lance && lance_percentual && lance_percentual > 0) {
|
| 82 |
+
valorLance = credito_desejado * lance_percentual;
|
| 83 |
+
const numeroParcelasAntecipadas = Math.floor(valorLance / parcelaIntegral.total);
|
| 84 |
+
prazoAposLance = Math.max(1, prazo_meses - numeroParcelasAntecipadas);
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
return {
|
| 88 |
+
credito: credito_desejado,
|
| 89 |
+
prazo: prazo_meses,
|
| 90 |
+
parcela_integral: parcelaIntegral,
|
| 91 |
+
parcela_reduzida: parcelaReduzida,
|
| 92 |
+
valor_lance: tem_lance ? valorLance : undefined,
|
| 93 |
+
prazo_apos_lance: tem_lance ? prazoAposLance : undefined,
|
| 94 |
+
};
|
| 95 |
+
}
|
packages/simulation-engine/tests/simulation-engine.test.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import test from "node:test";
|
| 2 |
+
import assert from "node:assert/strict";
|
| 3 |
+
import { calcularSimulacao } from "../src/index.js";
|
| 4 |
+
|
| 5 |
+
const baseInput = {
|
| 6 |
+
credito_desejado: 300000,
|
| 7 |
+
parametros: {
|
| 8 |
+
id_plano: "PADRAO",
|
| 9 |
+
nome_plano: "Plano Padrao Imovel",
|
| 10 |
+
prazo_meses: 180,
|
| 11 |
+
taxa_administracao_total: 0.15,
|
| 12 |
+
fundo_reserva_mensal: 0.0005,
|
| 13 |
+
seguro_prestamista: 0.035,
|
| 14 |
+
},
|
| 15 |
+
tem_lance: false,
|
| 16 |
+
};
|
| 17 |
+
|
| 18 |
+
test("calcula parcela integral com valores esperados", () => {
|
| 19 |
+
const result = calcularSimulacao(baseInput);
|
| 20 |
+
|
| 21 |
+
assert.equal(result.prazo, 180);
|
| 22 |
+
assert.equal(result.credito, 300000);
|
| 23 |
+
assert.ok(result.parcela_integral.total > 0);
|
| 24 |
+
assert.equal(Number(result.parcela_integral.fundo_comum.toFixed(2)), 1666.67);
|
| 25 |
+
});
|
| 26 |
+
|
| 27 |
+
test("calcula cenario com lance e reduz prazo", () => {
|
| 28 |
+
const result = calcularSimulacao({
|
| 29 |
+
...baseInput,
|
| 30 |
+
tem_lance: true,
|
| 31 |
+
lance_percentual: 0.3,
|
| 32 |
+
});
|
| 33 |
+
|
| 34 |
+
assert.ok(result.valor_lance);
|
| 35 |
+
assert.equal(result.valor_lance, 90000);
|
| 36 |
+
assert.ok(result.prazo_apos_lance);
|
| 37 |
+
assert.ok(result.prazo_apos_lance < result.prazo);
|
| 38 |
+
});
|
| 39 |
+
|
| 40 |
+
test("falha quando prazo_meses for invalido", () => {
|
| 41 |
+
assert.throws(() => {
|
| 42 |
+
calcularSimulacao({
|
| 43 |
+
...baseInput,
|
| 44 |
+
parametros: {
|
| 45 |
+
...baseInput.parametros,
|
| 46 |
+
prazo_meses: 0,
|
| 47 |
+
},
|
| 48 |
+
});
|
| 49 |
+
}, /prazo_meses/);
|
| 50 |
+
});
|