CARONTE v1.0 - Plataforma de Gestão Social

This commit is contained in:
2026-02-08 23:10:32 -03:00
commit c98c806865
60 changed files with 9450 additions and 0 deletions

0
backend/app/__init__.py Normal file
View File

View File

View File

View File

@@ -0,0 +1,38 @@
from fastapi import APIRouter, Depends, HTTPException
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.core.database import get_db
from app.core.security import hash_password, verify_password, create_access_token, get_current_user_id
from app.models.usuario import Usuario
from app.schemas.schemas import UserCreate, UserOut, Token
router = APIRouter(prefix="/auth", tags=["auth"])
@router.post("/registro", response_model=UserOut)
async def registro(data: UserCreate, db: AsyncSession = Depends(get_db)):
existing = await db.execute(select(Usuario).where(Usuario.email == data.email))
if existing.scalar_one_or_none():
raise HTTPException(400, "Email já cadastrado")
user = Usuario(nome=data.nome, email=data.email, senha_hash=hash_password(data.senha), telefone=data.telefone)
db.add(user)
await db.commit()
await db.refresh(user)
return user
@router.post("/login", response_model=Token)
async def login(form: OAuth2PasswordRequestForm = Depends(), db: AsyncSession = Depends(get_db)):
result = await db.execute(select(Usuario).where(Usuario.email == form.username))
user = result.scalar_one_or_none()
if not user or not verify_password(form.password, user.senha_hash):
raise HTTPException(401, "Credenciais inválidas")
token = create_access_token({"sub": str(user.id)})
return {"access_token": token}
@router.get("/me", response_model=UserOut)
async def me(user_id: int = Depends(get_current_user_id), db: AsyncSession = Depends(get_db)):
result = await db.execute(select(Usuario).where(Usuario.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(404, "Usuário não encontrado")
return user

View File

@@ -0,0 +1,32 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.core.database import get_db
from app.core.security import get_current_user_id
from app.models.beneficio import Beneficio
from app.models.falecido import Falecido
from app.schemas.schemas import BeneficioOut
from app.services.beneficio_scanner import escanear_beneficios
router = APIRouter(prefix="/familias/{familia_id}/beneficios", tags=["beneficios"])
@router.get("/", response_model=list[BeneficioOut])
async def listar(familia_id: int, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(Beneficio).where(Beneficio.familia_id == familia_id))
return result.scalars().all()
@router.post("/scan", response_model=list[BeneficioOut])
async def scan(familia_id: int, db: AsyncSession = Depends(get_db)):
# Delete existing and rescan
result = await db.execute(select(Beneficio).where(Beneficio.familia_id == familia_id))
for b in result.scalars().all():
await db.delete(b)
await db.commit()
result = await db.execute(select(Falecido).where(Falecido.familia_id == familia_id))
falecidos = result.scalars().all()
all_bens = []
for f in falecidos:
bens = await escanear_beneficios(db, familia_id, f)
all_bens.extend(bens)
return all_bens

View File

@@ -0,0 +1,41 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.core.database import get_db
from app.core.security import get_current_user_id
from app.models.checklist import ChecklistItem
from app.schemas.schemas import ChecklistItemOut, ChecklistUpdateStatus
from app.services.checklist_engine import get_proximo_passo
router = APIRouter(prefix="/familias/{familia_id}/checklist", tags=["checklist"])
@router.get("/", response_model=list[ChecklistItemOut])
async def listar(familia_id: int, db: AsyncSession = Depends(get_db)):
result = await db.execute(
select(ChecklistItem).where(ChecklistItem.familia_id == familia_id).order_by(ChecklistItem.ordem)
)
return result.scalars().all()
@router.put("/{item_id}", response_model=ChecklistItemOut)
async def atualizar_status(familia_id: int, item_id: int, data: ChecklistUpdateStatus, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(ChecklistItem).where(ChecklistItem.id == item_id, ChecklistItem.familia_id == familia_id))
item = result.scalar_one_or_none()
if not item:
raise HTTPException(404, "Item não encontrado")
item.status = data.status
await db.commit()
await db.refresh(item)
return item
@router.get("/proximo", response_model=ChecklistItemOut | None)
async def proximo_passo(familia_id: int, db: AsyncSession = Depends(get_db)):
result = await db.execute(
select(ChecklistItem).where(ChecklistItem.familia_id == familia_id).order_by(ChecklistItem.ordem)
)
items = [{"id": i.id, "status": i.status, "ordem": i.ordem, "titulo": i.titulo, "descricao": i.descricao,
"fase": i.fase, "categoria": i.categoria, "prazo_dias": i.prazo_dias, "familia_id": i.familia_id,
"falecido_id": i.falecido_id, "dependencia_id": i.dependencia_id} for i in result.scalars().all()]
p = get_proximo_passo(items)
if not p:
return None
return p

View File

@@ -0,0 +1,54 @@
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from app.core.database import get_db
from app.core.security import get_current_user_id
from app.models.familia import Familia
from app.models.checklist import ChecklistItem
from app.models.beneficio import Beneficio
from app.models.documento import Documento
from app.schemas.schemas import DashboardOut
router = APIRouter(prefix="/dashboard", tags=["dashboard"])
@router.get("/", response_model=DashboardOut)
async def dashboard(user_id: int = Depends(get_current_user_id), db: AsyncSession = Depends(get_db)):
fams = await db.execute(select(Familia).where(Familia.usuario_id == user_id))
familias = fams.scalars().all()
fam_ids = [f.id for f in familias]
pendentes = 0
bens_count = 0
docs_count = 0
fam_list = []
for fam in familias:
ch = await db.execute(select(ChecklistItem).where(ChecklistItem.familia_id == fam.id))
items = ch.scalars().all()
total = len(items)
done = len([i for i in items if i.status == "concluido"])
pendentes += len([i for i in items if i.status == "pendente"])
bn = await db.execute(select(func.count()).select_from(Beneficio).where(Beneficio.familia_id == fam.id))
bc = bn.scalar() or 0
bens_count += bc
dc = await db.execute(select(func.count()).select_from(Documento).where(Documento.familia_id == fam.id))
docs_count += dc.scalar() or 0
fam_list.append({
"id": fam.id,
"nome": fam.nome,
"total_items": total,
"concluidos": done,
"progresso": round(done / total * 100) if total else 0,
"beneficios": bc,
})
return DashboardOut(
familias_ativas=len(familias),
itens_pendentes=pendentes,
beneficios_encontrados=bens_count,
documentos_gerados=docs_count,
familias=fam_list,
)

View File

@@ -0,0 +1,62 @@
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import FileResponse
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.core.database import get_db
from app.core.security import get_current_user_id
from app.models.documento import Documento
from app.models.falecido import Falecido
from app.models.familia import Familia, MembroFamilia
from app.schemas.schemas import DocumentoOut, DocumentoGerarRequest
from app.services.document_generator import GENERATORS
import os
router = APIRouter(prefix="/familias/{familia_id}/documentos", tags=["documentos"])
@router.get("/", response_model=list[DocumentoOut])
async def listar(familia_id: int, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(Documento).where(Documento.familia_id == familia_id))
return result.scalars().all()
@router.post("/gerar", response_model=DocumentoOut)
async def gerar(familia_id: int, data: DocumentoGerarRequest, db: AsyncSession = Depends(get_db)):
gen = GENERATORS.get(data.tipo)
if not gen:
raise HTTPException(400, f"Tipo de documento inválido: {data.tipo}")
fam = await db.execute(select(Familia).where(Familia.id == familia_id))
familia = fam.scalar_one_or_none()
if not familia:
raise HTTPException(404, "Família não encontrada")
fal = await db.execute(select(Falecido).where(Falecido.id == data.falecido_id))
falecido = fal.scalar_one_or_none()
if not falecido:
raise HTTPException(404, "Falecido não encontrado")
membros = await db.execute(select(MembroFamilia).where(MembroFamilia.familia_id == familia_id))
membro = membros.scalars().first()
membro_nome = membro.nome if membro else "Representante da Família"
path = gen(familia.nome, falecido.nome, membro_nome, falecido.cpf or "000.000.000-00")
doc = Documento(
familia_id=familia_id,
falecido_id=data.falecido_id,
tipo=data.tipo,
nome=f"{data.tipo.replace('_',' ').title()} - {falecido.nome}",
arquivo_path=path,
status="gerado",
)
db.add(doc)
await db.commit()
await db.refresh(doc)
return doc
@router.get("/{doc_id}/download")
async def download(familia_id: int, doc_id: int, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(Documento).where(Documento.id == doc_id, Documento.familia_id == familia_id))
doc = result.scalar_one_or_none()
if not doc or not doc.arquivo_path or not os.path.exists(doc.arquivo_path):
raise HTTPException(404, "Documento não encontrado")
return FileResponse(doc.arquivo_path, filename=os.path.basename(doc.arquivo_path), media_type="application/pdf")

View File

@@ -0,0 +1,61 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.core.database import get_db
from app.core.security import get_current_user_id
from app.models.familia import Familia, MembroFamilia
from app.models.falecido import Falecido
from app.schemas.schemas import FamiliaCreate, FamiliaOut, MembroCreate, MembroOut, FalecidoCreate, FalecidoOut
from app.services.checklist_engine import gerar_checklist
from app.services.beneficio_scanner import escanear_beneficios
router = APIRouter(prefix="/familias", tags=["familias"])
@router.get("/", response_model=list[FamiliaOut])
async def listar(user_id: int = Depends(get_current_user_id), db: AsyncSession = Depends(get_db)):
result = await db.execute(select(Familia).where(Familia.usuario_id == user_id))
return result.scalars().all()
@router.post("/", response_model=FamiliaOut)
async def criar(data: FamiliaCreate, user_id: int = Depends(get_current_user_id), db: AsyncSession = Depends(get_db)):
f = Familia(nome=data.nome, usuario_id=user_id)
db.add(f)
await db.commit()
await db.refresh(f)
return f
@router.get("/{id}", response_model=FamiliaOut)
async def detalhe(id: int, user_id: int = Depends(get_current_user_id), db: AsyncSession = Depends(get_db)):
result = await db.execute(select(Familia).where(Familia.id == id, Familia.usuario_id == user_id))
f = result.scalar_one_or_none()
if not f:
raise HTTPException(404, "Família não encontrada")
return f
@router.post("/{id}/membros", response_model=MembroOut)
async def add_membro(id: int, data: MembroCreate, db: AsyncSession = Depends(get_db)):
m = MembroFamilia(familia_id=id, **data.model_dump())
db.add(m)
await db.commit()
await db.refresh(m)
return m
@router.get("/{id}/membros", response_model=list[MembroOut])
async def listar_membros(id: int, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(MembroFamilia).where(MembroFamilia.familia_id == id))
return result.scalars().all()
@router.post("/{id}/falecido", response_model=FalecidoOut)
async def registrar_falecido(id: int, data: FalecidoCreate, db: AsyncSession = Depends(get_db)):
f = Falecido(familia_id=id, **data.model_dump())
db.add(f)
await db.commit()
await db.refresh(f)
await gerar_checklist(db, id, f)
await escanear_beneficios(db, id, f)
return f
@router.get("/{id}/falecidos", response_model=list[FalecidoOut])
async def listar_falecidos(id: int, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(Falecido).where(Falecido.familia_id == id))
return result.scalars().all()

View File

View File

@@ -0,0 +1,14 @@
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
APP_NAME: str = "CARONTE"
SECRET_KEY: str = "caronte-secret-key-change-in-production"
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 1440
DATABASE_URL: str = "sqlite+aiosqlite:///./caronte.db"
UPLOAD_DIR: str = "./uploads"
class Config:
env_file = ".env"
settings = Settings()

View File

@@ -0,0 +1,17 @@
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase
from app.core.config import settings
engine = create_async_engine(settings.DATABASE_URL, echo=False)
async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
class Base(DeclarativeBase):
pass
async def get_db():
async with async_session() as session:
yield session
async def init_db():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)

View File

@@ -0,0 +1,31 @@
from datetime import datetime, timedelta
from jose import JWTError, jwt
from passlib.context import CryptContext
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from app.core.config import settings
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def verify_password(plain: str, hashed: str) -> bool:
return pwd_context.verify(plain, hashed)
def create_access_token(data: dict) -> str:
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
async def get_current_user_id(token: str = Depends(oauth2_scheme)) -> int:
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
user_id = payload.get("sub")
if user_id is None:
raise HTTPException(status_code=401, detail="Token inválido")
return int(user_id)
except JWTError:
raise HTTPException(status_code=401, detail="Token inválido")

View File

@@ -0,0 +1,182 @@
[
{
"titulo": "Obter Declaração de Óbito",
"descricao": "Solicitar a declaração de óbito no hospital ou IML onde ocorreu o falecimento.",
"fase": "imediato",
"categoria": "certidoes",
"prazo_dias": 1,
"ordem": 1,
"condicao": null
},
{
"titulo": "Registrar Certidão de Óbito",
"descricao": "Levar a declaração de óbito ao cartório de registro civil para emitir a certidão de óbito.",
"fase": "imediato",
"categoria": "certidoes",
"prazo_dias": 2,
"ordem": 2,
"condicao": null
},
{
"titulo": "Providenciar Sepultamento/Cremação",
"descricao": "Contratar serviço funerário e realizar o sepultamento ou cremação.",
"fase": "imediato",
"categoria": "certidoes",
"prazo_dias": 3,
"ordem": 3,
"condicao": null
},
{
"titulo": "Comunicar Empregador",
"descricao": "Informar o empregador sobre o falecimento para rescisão do contrato de trabalho.",
"fase": "primeira_semana",
"categoria": "trabalhista",
"prazo_dias": 7,
"ordem": 4,
"condicao": "tinha_carteira_assinada"
},
{
"titulo": "Comunicar INSS",
"descricao": "Informar o INSS sobre o óbito e verificar benefícios como pensão por morte.",
"fase": "primeira_semana",
"categoria": "inss",
"prazo_dias": 7,
"ordem": 5,
"condicao": null
},
{
"titulo": "Solicitar Pensão por Morte",
"descricao": "Requerer pensão por morte no INSS (Meu INSS ou agência). Prazo ideal: até 90 dias do óbito para receber desde a data do falecimento.",
"fase": "primeira_semana",
"categoria": "inss",
"prazo_dias": 90,
"ordem": 6,
"condicao": null
},
{
"titulo": "Solicitar Saque FGTS",
"descricao": "Comparecer à Caixa Econômica Federal com documentos para saque do FGTS do falecido.",
"fase": "primeira_semana",
"categoria": "fgts",
"prazo_dias": 14,
"ordem": 7,
"condicao": "tinha_fgts"
},
{
"titulo": "Solicitar Saque PIS/PASEP",
"descricao": "Verificar saldo PIS/PASEP e solicitar saque na Caixa (PIS) ou Banco do Brasil (PASEP).",
"fase": "primeira_semana",
"categoria": "fgts",
"prazo_dias": 14,
"ordem": 8,
"condicao": "tinha_carteira_assinada"
},
{
"titulo": "Comunicar Bancos",
"descricao": "Informar os bancos onde o falecido tinha conta sobre o óbito. Solicitar extrato e bloqueio.",
"fase": "primeira_semana",
"categoria": "financeiro",
"prazo_dias": 10,
"ordem": 9,
"condicao": null
},
{
"titulo": "Acionar Seguro de Vida",
"descricao": "Entrar em contato com a seguradora para acionar o seguro de vida.",
"fase": "primeira_semana",
"categoria": "seguros",
"prazo_dias": 10,
"ordem": 10,
"condicao": "tinha_seguro_vida"
},
{
"titulo": "Solicitar DPVAT (se acidente)",
"descricao": "Se o óbito foi por acidente de trânsito, solicitar indenização do DPVAT.",
"fase": "30_dias",
"categoria": "seguros",
"prazo_dias": 30,
"ordem": 11,
"condicao": null
},
{
"titulo": "Consultar Receita Federal (e-CAC)",
"descricao": "Verificar pendências fiscais do falecido e situação do CPF.",
"fase": "30_dias",
"categoria": "fiscal",
"prazo_dias": 30,
"ordem": 12,
"condicao": null
},
{
"titulo": "Transferir Veículos",
"descricao": "Iniciar processo de transferência de veículos no DETRAN.",
"fase": "30_dias",
"categoria": "patrimonio",
"prazo_dias": 30,
"ordem": 13,
"condicao": "tinha_veiculos"
},
{
"titulo": "Iniciar Inventário",
"descricao": "Contratar advogado e iniciar inventário judicial ou extrajudicial. Prazo legal: 60 dias do óbito.",
"fase": "30_dias",
"categoria": "inventario",
"prazo_dias": 60,
"ordem": 14,
"condicao": null
},
{
"titulo": "Levantar Bens Imóveis",
"descricao": "Solicitar certidões de matrícula dos imóveis nos cartórios de registro de imóveis.",
"fase": "30_dias",
"categoria": "inventario",
"prazo_dias": 45,
"ordem": 15,
"condicao": "tinha_imoveis"
},
{
"titulo": "Fazer Declaração Final de Espólio (IR)",
"descricao": "Declarar imposto de renda do falecido referente ao ano do óbito (declaração inicial de espólio).",
"fase": "60_dias",
"categoria": "fiscal",
"prazo_dias": 180,
"ordem": 16,
"condicao": null
},
{
"titulo": "Cancelar CPF do Falecido",
"descricao": "Solicitar a regularização do CPF como 'titular falecido' na Receita Federal.",
"fase": "60_dias",
"categoria": "fiscal",
"prazo_dias": 90,
"ordem": 17,
"condicao": null
},
{
"titulo": "Cancelar Serviços e Assinaturas",
"descricao": "Cancelar telefone, internet, plano de saúde, assinaturas e cartões de crédito.",
"fase": "30_dias",
"categoria": "administrativo",
"prazo_dias": 30,
"ordem": 18,
"condicao": null
},
{
"titulo": "Transferir Titularidade de Serviços",
"descricao": "Transferir contas de água, luz, gás para nome de familiar.",
"fase": "30_dias",
"categoria": "administrativo",
"prazo_dias": 45,
"ordem": 19,
"condicao": null
},
{
"titulo": "Concluir Partilha de Bens",
"descricao": "Finalizar o inventário com a partilha formal dos bens entre os herdeiros.",
"fase": "60_dias",
"categoria": "inventario",
"prazo_dias": 365,
"ordem": 20,
"condicao": null
}
]

31
backend/app/main.py Normal file
View File

@@ -0,0 +1,31 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
from app.core.database import init_db
from app.api.v1 import auth, familias, checklist, beneficios, documentos, dashboard
@asynccontextmanager
async def lifespan(app: FastAPI):
await init_db()
yield
app = FastAPI(title="CARONTE API", description="O barqueiro que guia famílias pelo rio burocrático pós-óbito", version="1.0.0", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(auth.router, prefix="/api/v1")
app.include_router(familias.router, prefix="/api/v1")
app.include_router(checklist.router, prefix="/api/v1")
app.include_router(beneficios.router, prefix="/api/v1")
app.include_router(documentos.router, prefix="/api/v1")
app.include_router(dashboard.router, prefix="/api/v1")
@app.get("/")
async def root():
return {"message": "🚣 CARONTE - Guiando famílias pelo rio burocrático", "docs": "/docs"}

View File

@@ -0,0 +1,6 @@
from app.models.usuario import Usuario
from app.models.familia import Familia, MembroFamilia
from app.models.falecido import Falecido
from app.models.checklist import ChecklistItem
from app.models.beneficio import Beneficio
from app.models.documento import Documento

View File

@@ -0,0 +1,16 @@
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Float
from sqlalchemy.sql import func
from app.core.database import Base
class Beneficio(Base):
__tablename__ = "beneficios"
id = Column(Integer, primary_key=True, autoincrement=True)
familia_id = Column(Integer, ForeignKey("familias.id"), nullable=False)
falecido_id = Column(Integer, ForeignKey("falecidos.id"), nullable=False)
tipo = Column(String, nullable=False) # fgts, pis, pensao_morte, seguro_vida, seguro_dpvat
nome = Column(String, nullable=False)
descricao = Column(String, nullable=True)
status = Column(String, default="identificado") # identificado, em_processo, sacado
valor_estimado = Column(Float, nullable=True)
valor_sacado = Column(Float, nullable=True)
created_at = Column(DateTime, server_default=func.now())

View File

@@ -0,0 +1,19 @@
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Text
from sqlalchemy.sql import func
from app.core.database import Base
class ChecklistItem(Base):
__tablename__ = "checklist_items"
id = Column(Integer, primary_key=True, autoincrement=True)
familia_id = Column(Integer, ForeignKey("familias.id"), nullable=False)
falecido_id = Column(Integer, ForeignKey("falecidos.id"), nullable=False)
titulo = Column(String, nullable=False)
descricao = Column(Text, nullable=True)
fase = Column(String, nullable=False) # imediato, primeira_semana, 30_dias, 60_dias
categoria = Column(String, nullable=False) # certidoes, inss, fgts, inventario, etc
status = Column(String, default="pendente") # pendente, andamento, concluido
prazo_dias = Column(Integer, nullable=True)
ordem = Column(Integer, default=0)
dependencia_id = Column(Integer, nullable=True)
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
created_at = Column(DateTime, server_default=func.now())

View File

@@ -0,0 +1,14 @@
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
from sqlalchemy.sql import func
from app.core.database import Base
class Documento(Base):
__tablename__ = "documentos"
id = Column(Integer, primary_key=True, autoincrement=True)
familia_id = Column(Integer, ForeignKey("familias.id"), nullable=False)
falecido_id = Column(Integer, ForeignKey("falecidos.id"), nullable=True)
tipo = Column(String, nullable=False) # procuracao, requerimento_fgts, peticao_pensao, alvara
nome = Column(String, nullable=False)
arquivo_path = Column(String, nullable=True)
status = Column(String, default="gerado") # gerado, enviado, aprovado
created_at = Column(DateTime, server_default=func.now())

View File

@@ -0,0 +1,23 @@
from sqlalchemy import Column, Integer, String, DateTime, Date, ForeignKey, Float
from sqlalchemy.sql import func
from app.core.database import Base
class Falecido(Base):
__tablename__ = "falecidos"
id = Column(Integer, primary_key=True, autoincrement=True)
familia_id = Column(Integer, ForeignKey("familias.id"), nullable=False)
nome = Column(String, nullable=False)
cpf = Column(String, nullable=True)
data_nascimento = Column(Date, nullable=True)
data_obito = Column(Date, nullable=False)
causa_obito = Column(String, nullable=True)
tipo_vinculo = Column(String, nullable=False) # empregado, aposentado, autonomo, servidor
empregador = Column(String, nullable=True)
tinha_carteira_assinada = Column(Integer, default=0)
tinha_fgts = Column(Integer, default=0)
era_aposentado = Column(Integer, default=0)
tinha_seguro_vida = Column(Integer, default=0)
tinha_imoveis = Column(Integer, default=0)
tinha_veiculos = Column(Integer, default=0)
salario_estimado = Column(Float, nullable=True)
created_at = Column(DateTime, server_default=func.now())

View File

@@ -0,0 +1,20 @@
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
from sqlalchemy.sql import func
from app.core.database import Base
class Familia(Base):
__tablename__ = "familias"
id = Column(Integer, primary_key=True, autoincrement=True)
nome = Column(String, nullable=False)
usuario_id = Column(Integer, ForeignKey("usuarios.id"), nullable=False)
created_at = Column(DateTime, server_default=func.now())
class MembroFamilia(Base):
__tablename__ = "membros_familia"
id = Column(Integer, primary_key=True, autoincrement=True)
familia_id = Column(Integer, ForeignKey("familias.id"), nullable=False)
nome = Column(String, nullable=False)
parentesco = Column(String, nullable=False)
cpf = Column(String, nullable=True)
telefone = Column(String, nullable=True)
email = Column(String, nullable=True)

View File

@@ -0,0 +1,12 @@
from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.sql import func
from app.core.database import Base
class Usuario(Base):
__tablename__ = "usuarios"
id = Column(Integer, primary_key=True, autoincrement=True)
nome = Column(String, nullable=False)
email = Column(String, unique=True, nullable=False)
senha_hash = Column(String, nullable=False)
telefone = Column(String, nullable=True)
created_at = Column(DateTime, server_default=func.now())

View File

View File

@@ -0,0 +1,151 @@
from pydantic import BaseModel
from typing import Optional
from datetime import date, datetime
# Auth
class UserCreate(BaseModel):
nome: str
email: str
senha: str
telefone: Optional[str] = None
class UserLogin(BaseModel):
email: str
senha: str
class UserOut(BaseModel):
id: int
nome: str
email: str
telefone: Optional[str] = None
class Config:
from_attributes = True
class Token(BaseModel):
access_token: str
token_type: str = "bearer"
# Familia
class FamiliaCreate(BaseModel):
nome: str
class FamiliaOut(BaseModel):
id: int
nome: str
usuario_id: int
created_at: Optional[datetime] = None
class Config:
from_attributes = True
class MembroCreate(BaseModel):
nome: str
parentesco: str
cpf: Optional[str] = None
telefone: Optional[str] = None
email: Optional[str] = None
class MembroOut(BaseModel):
id: int
familia_id: int
nome: str
parentesco: str
cpf: Optional[str] = None
telefone: Optional[str] = None
email: Optional[str] = None
class Config:
from_attributes = True
# Falecido
class FalecidoCreate(BaseModel):
nome: str
cpf: Optional[str] = None
data_nascimento: Optional[date] = None
data_obito: date
causa_obito: Optional[str] = None
tipo_vinculo: str
empregador: Optional[str] = None
tinha_carteira_assinada: int = 0
tinha_fgts: int = 0
era_aposentado: int = 0
tinha_seguro_vida: int = 0
tinha_imoveis: int = 0
tinha_veiculos: int = 0
salario_estimado: Optional[float] = None
class FalecidoOut(BaseModel):
id: int
familia_id: int
nome: str
cpf: Optional[str] = None
data_nascimento: Optional[date] = None
data_obito: date
causa_obito: Optional[str] = None
tipo_vinculo: str
empregador: Optional[str] = None
tinha_carteira_assinada: int = 0
tinha_fgts: int = 0
era_aposentado: int = 0
tinha_seguro_vida: int = 0
tinha_imoveis: int = 0
tinha_veiculos: int = 0
salario_estimado: Optional[float] = None
class Config:
from_attributes = True
# Checklist
class ChecklistItemOut(BaseModel):
id: int
familia_id: int
falecido_id: int
titulo: str
descricao: Optional[str] = None
fase: str
categoria: str
status: str
prazo_dias: Optional[int] = None
ordem: int = 0
dependencia_id: Optional[int] = None
class Config:
from_attributes = True
class ChecklistUpdateStatus(BaseModel):
status: str
# Beneficio
class BeneficioOut(BaseModel):
id: int
familia_id: int
falecido_id: int
tipo: str
nome: str
descricao: Optional[str] = None
status: str
valor_estimado: Optional[float] = None
valor_sacado: Optional[float] = None
class Config:
from_attributes = True
# Documento
class DocumentoOut(BaseModel):
id: int
familia_id: int
falecido_id: Optional[int] = None
tipo: str
nome: str
arquivo_path: Optional[str] = None
status: str
created_at: Optional[datetime] = None
class Config:
from_attributes = True
class DocumentoGerarRequest(BaseModel):
tipo: str
falecido_id: int
# Dashboard
class DashboardOut(BaseModel):
familias_ativas: int
itens_pendentes: int
beneficios_encontrados: int
documentos_gerados: int
familias: list = []

View File

View File

@@ -0,0 +1,64 @@
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.beneficio import Beneficio
from app.models.falecido import Falecido
BENEFICIOS_MAP = [
{
"condicao": "tinha_fgts",
"tipo": "fgts",
"nome": "Saque FGTS",
"descricao": "Saldo do FGTS disponível para saque pelos dependentes habilitados.",
"valor_mult": 3.5,
},
{
"condicao": "tinha_carteira_assinada",
"tipo": "pis",
"nome": "Saque PIS/PASEP",
"descricao": "Saldo de cotas do PIS/PASEP para saque pelos herdeiros.",
"valor_mult": 0.8,
},
{
"condicao": None,
"tipo": "pensao_morte",
"nome": "Pensão por Morte (INSS)",
"descricao": "Benefício mensal pago pelo INSS aos dependentes do segurado falecido.",
"valor_mult": 12,
},
{
"condicao": "tinha_seguro_vida",
"tipo": "seguro_vida",
"nome": "Seguro de Vida",
"descricao": "Indenização do seguro de vida contratado pelo falecido.",
"valor_mult": 24,
},
{
"condicao": "tinha_carteira_assinada",
"tipo": "rescisao",
"nome": "Verbas Rescisórias",
"descricao": "Saldo de salário, férias proporcionais, 13º proporcional.",
"valor_mult": 2,
},
]
async def escanear_beneficios(db: AsyncSession, familia_id: int, falecido: Falecido) -> list[Beneficio]:
salario = falecido.salario_estimado or 2500.0
beneficios = []
for b in BENEFICIOS_MAP:
cond = b.get("condicao")
if cond:
val = getattr(falecido, cond, 0)
if not val:
continue
ben = Beneficio(
familia_id=familia_id,
falecido_id=falecido.id,
tipo=b["tipo"],
nome=b["nome"],
descricao=b["descricao"],
status="identificado",
valor_estimado=round(salario * b["valor_mult"], 2),
)
db.add(ben)
beneficios.append(ben)
await db.commit()
return beneficios

View File

@@ -0,0 +1,43 @@
import json
import os
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.checklist import ChecklistItem
from app.models.falecido import Falecido
TEMPLATES_PATH = os.path.join(os.path.dirname(__file__), "..", "data", "checklist_templates.json")
def load_templates():
with open(TEMPLATES_PATH, "r", encoding="utf-8") as f:
return json.load(f)
async def gerar_checklist(db: AsyncSession, familia_id: int, falecido: Falecido) -> list[ChecklistItem]:
templates = load_templates()
items = []
for t in templates:
cond = t.get("condicao")
if cond:
val = getattr(falecido, cond, 0)
if not val:
continue
item = ChecklistItem(
familia_id=familia_id,
falecido_id=falecido.id,
titulo=t["titulo"],
descricao=t.get("descricao"),
fase=t["fase"],
categoria=t["categoria"],
status="pendente",
prazo_dias=t.get("prazo_dias"),
ordem=t.get("ordem", 0),
)
db.add(item)
items.append(item)
await db.commit()
return items
def get_proximo_passo(items: list[dict]) -> dict | None:
pendentes = [i for i in items if i.get("status") == "pendente"]
if not pendentes:
return None
pendentes.sort(key=lambda x: x.get("ordem", 999))
return pendentes[0]

View File

@@ -0,0 +1,99 @@
import os
from datetime import datetime
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import cm
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY
from app.core.config import settings
os.makedirs(settings.UPLOAD_DIR, exist_ok=True)
def _styles():
ss = getSampleStyleSheet()
ss.add(ParagraphStyle(name="TitleCenter", parent=ss["Title"], alignment=TA_CENTER, fontSize=16))
ss.add(ParagraphStyle(name="Body", parent=ss["Normal"], alignment=TA_JUSTIFY, fontSize=11, leading=16))
return ss
def gerar_procuracao(familia_nome: str, falecido_nome: str, membro_nome: str, falecido_cpf: str = "000.000.000-00") -> str:
filename = f"procuracao_{familia_nome.lower().replace(' ','_')}_{datetime.now().strftime('%Y%m%d%H%M%S')}.pdf"
path = os.path.join(settings.UPLOAD_DIR, filename)
doc = SimpleDocTemplate(path, pagesize=A4, topMargin=3*cm)
ss = _styles()
story = [
Paragraph("PROCURAÇÃO", ss["TitleCenter"]),
Spacer(1, 1*cm),
Paragraph(
f"Pelo presente instrumento particular, eu, <b>{membro_nome}</b>, na qualidade de herdeiro(a) "
f"de <b>{falecido_nome}</b> (CPF: {falecido_cpf}), da família <b>{familia_nome}</b>, nomeio e constituo "
f"como meu(minha) bastante procurador(a) ______________________, para em meu nome representar "
f"perante órgãos públicos, bancos e demais instituições, podendo requerer, assinar documentos, "
f"receber valores e praticar todos os atos necessários ao cumprimento deste mandato.",
ss["Body"]
),
Spacer(1, 1.5*cm),
Paragraph(f"Local e data: ________________, {datetime.now().strftime('%d/%m/%Y')}", ss["Body"]),
Spacer(1, 2*cm),
Paragraph("_______________________________<br/>Assinatura do Outorgante", ss["Body"]),
]
doc.build(story)
return path
def gerar_requerimento_fgts(familia_nome: str, falecido_nome: str, membro_nome: str, falecido_cpf: str = "000.000.000-00") -> str:
filename = f"requerimento_fgts_{familia_nome.lower().replace(' ','_')}_{datetime.now().strftime('%Y%m%d%H%M%S')}.pdf"
path = os.path.join(settings.UPLOAD_DIR, filename)
doc = SimpleDocTemplate(path, pagesize=A4, topMargin=3*cm)
ss = _styles()
story = [
Paragraph("REQUERIMENTO DE SAQUE DO FGTS POR FALECIMENTO", ss["TitleCenter"]),
Spacer(1, 1*cm),
Paragraph("À Caixa Econômica Federal", ss["Body"]),
Spacer(1, 0.5*cm),
Paragraph(
f"Eu, <b>{membro_nome}</b>, na qualidade de dependente/herdeiro(a) de <b>{falecido_nome}</b> "
f"(CPF: {falecido_cpf}), venho por meio deste requerer o saque do saldo da(s) conta(s) vinculada(s) "
f"ao FGTS do trabalhador falecido, conforme previsto na Lei nº 8.036/90, art. 20, inciso IV.",
ss["Body"]
),
Spacer(1, 0.5*cm),
Paragraph("Documentos em anexo: Certidão de Óbito, documento de identidade, comprovante de dependência.", ss["Body"]),
Spacer(1, 1.5*cm),
Paragraph(f"Local e data: ________________, {datetime.now().strftime('%d/%m/%Y')}", ss["Body"]),
Spacer(1, 2*cm),
Paragraph("_______________________________<br/>Assinatura do Requerente", ss["Body"]),
]
doc.build(story)
return path
def gerar_peticao_pensao(familia_nome: str, falecido_nome: str, membro_nome: str, falecido_cpf: str = "000.000.000-00") -> str:
filename = f"peticao_pensao_{familia_nome.lower().replace(' ','_')}_{datetime.now().strftime('%Y%m%d%H%M%S')}.pdf"
path = os.path.join(settings.UPLOAD_DIR, filename)
doc = SimpleDocTemplate(path, pagesize=A4, topMargin=3*cm)
ss = _styles()
story = [
Paragraph("REQUERIMENTO DE PENSÃO POR MORTE", ss["TitleCenter"]),
Spacer(1, 1*cm),
Paragraph("Ao Instituto Nacional do Seguro Social - INSS", ss["Body"]),
Spacer(1, 0.5*cm),
Paragraph(
f"Eu, <b>{membro_nome}</b>, na qualidade de dependente de <b>{falecido_nome}</b> "
f"(CPF: {falecido_cpf}), venho requerer a concessão do benefício de Pensão por Morte, "
f"conforme previsto nos arts. 74 a 79 da Lei nº 8.213/91, em razão do falecimento do(a) "
f"segurado(a) acima identificado(a).",
ss["Body"]
),
Spacer(1, 0.5*cm),
Paragraph("Documentos em anexo: Certidão de Óbito, documentos pessoais, comprovante de dependência econômica.", ss["Body"]),
Spacer(1, 1.5*cm),
Paragraph(f"Local e data: ________________, {datetime.now().strftime('%d/%m/%Y')}", ss["Body"]),
Spacer(1, 2*cm),
Paragraph("_______________________________<br/>Assinatura do Requerente", ss["Body"]),
]
doc.build(story)
return path
GENERATORS = {
"procuracao": gerar_procuracao,
"requerimento_fgts": gerar_requerimento_fgts,
"peticao_pensao": gerar_peticao_pensao,
}

11
backend/requirements.txt Normal file
View File

@@ -0,0 +1,11 @@
fastapi==0.109.0
uvicorn[standard]==0.27.0
sqlalchemy[asyncio]==2.0.25
aiosqlite==0.19.0
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
python-multipart==0.0.6
reportlab==4.1.0
pydantic-settings==2.1.0
bcrypt==4.0.1
asyncpg==0.30.0

98
backend/seed_data.py Normal file
View File

@@ -0,0 +1,98 @@
"""Seed script - popula o banco com dados mock."""
import asyncio
from datetime import date
from app.core.database import engine, async_session, Base
from app.core.security import hash_password
from app.models import *
from app.services.checklist_engine import gerar_checklist
from app.services.beneficio_scanner import escanear_beneficios
async def seed():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all)
async with async_session() as db:
# Usuário demo
user = Usuario(nome="Maria da Silva", email="demo@caronte.com.br", senha_hash=hash_password("123456"), telefone="(11) 99999-0001")
db.add(user)
await db.commit()
await db.refresh(user)
# Famílias
familias_data = [
{"nome": "Família Silva", "membros": [
{"nome": "Maria da Silva", "parentesco": "Esposa", "cpf": "123.456.789-00", "telefone": "(11) 99999-0001"},
{"nome": "João da Silva Jr.", "parentesco": "Filho", "cpf": "123.456.789-01"},
]},
{"nome": "Família Oliveira", "membros": [
{"nome": "Ana Oliveira", "parentesco": "Filha", "cpf": "234.567.890-00", "telefone": "(21) 98888-0002"},
{"nome": "Carlos Oliveira", "parentesco": "Filho", "cpf": "234.567.890-01"},
]},
{"nome": "Família Santos", "membros": [
{"nome": "Lucia Santos", "parentesco": "Esposa", "cpf": "345.678.901-00", "telefone": "(31) 97777-0003"},
]},
]
falecidos_data = [
{"familia_idx": 0, "nome": "José da Silva", "cpf": "111.222.333-44", "data_nascimento": date(1955, 3, 15),
"data_obito": date(2026, 1, 10), "causa_obito": "Infarto agudo do miocárdio", "tipo_vinculo": "empregado",
"empregador": "Construtora Progresso Ltda", "tinha_carteira_assinada": 1, "tinha_fgts": 1,
"era_aposentado": 0, "tinha_seguro_vida": 1, "tinha_imoveis": 1, "tinha_veiculos": 1, "salario_estimado": 4500.0},
{"familia_idx": 0, "nome": "Antônia da Silva", "cpf": "111.222.333-55", "data_nascimento": date(1930, 8, 20),
"data_obito": date(2025, 11, 5), "causa_obito": "Causas naturais", "tipo_vinculo": "aposentado",
"era_aposentado": 1, "salario_estimado": 2800.0},
{"familia_idx": 1, "nome": "Roberto Oliveira", "cpf": "222.333.444-55", "data_nascimento": date(1960, 12, 1),
"data_obito": date(2026, 1, 25), "causa_obito": "Câncer de pulmão", "tipo_vinculo": "aposentado",
"era_aposentado": 1, "tinha_seguro_vida": 1, "tinha_imoveis": 1, "salario_estimado": 5200.0},
{"familia_idx": 1, "nome": "Pedro Oliveira", "cpf": "222.333.444-66", "data_nascimento": date(1985, 5, 10),
"data_obito": date(2026, 2, 1), "causa_obito": "Acidente de trânsito", "tipo_vinculo": "empregado",
"empregador": "Tech Solutions SA", "tinha_carteira_assinada": 1, "tinha_fgts": 1,
"tinha_seguro_vida": 0, "tinha_veiculos": 1, "salario_estimado": 8500.0},
{"familia_idx": 2, "nome": "Francisco Santos", "cpf": "333.444.555-66", "data_nascimento": date(1970, 7, 22),
"data_obito": date(2026, 1, 15), "causa_obito": "AVC", "tipo_vinculo": "autonomo",
"tinha_carteira_assinada": 0, "tinha_fgts": 0, "tinha_imoveis": 1, "salario_estimado": 3500.0},
]
familias = []
for fd in familias_data:
f = Familia(nome=fd["nome"], usuario_id=user.id)
db.add(f)
await db.commit()
await db.refresh(f)
familias.append(f)
for m in fd["membros"]:
db.add(MembroFamilia(familia_id=f.id, **m))
await db.commit()
for fd in falecidos_data:
fam = familias[fd["familia_idx"]]
data = {k: v for k, v in fd.items() if k != "familia_idx"}
fal = Falecido(familia_id=fam.id, **data)
db.add(fal)
await db.commit()
await db.refresh(fal)
await gerar_checklist(db, fam.id, fal)
await escanear_beneficios(db, fam.id, fal)
# Documentos mock
doc_types = [
(familias[0].id, 1, "procuracao", "Procuração - José da Silva"),
(familias[0].id, 1, "requerimento_fgts", "Requerimento FGTS - José da Silva"),
(familias[1].id, 3, "peticao_pensao", "Petição Pensão por Morte - Roberto Oliveira"),
]
for fam_id, fal_id, tipo, nome in doc_types:
db.add(Documento(familia_id=fam_id, falecido_id=fal_id, tipo=tipo, nome=nome, status="gerado"))
await db.commit()
print("✅ Seed concluído! Dados mock inseridos.")
print(" - 1 usuário: demo@caronte.com.br / 123456")
print(" - 3 famílias, 5 falecidos")
print(" - Checklists e benefícios gerados automaticamente")
if __name__ == "__main__":
asyncio.run(seed())