Initial project import

This commit is contained in:
Tiago
2026-09-29 12:12:31 -03:00
commit c435bb5e23
120 changed files with 5371 additions and 0 deletions
@@ -0,0 +1,215 @@
# Sistema de Contabilidade de Condomínios — Arquitetura v3.0 (FINAL)
> Substitui v2.0. Escopo drasticamente simplificado: o sistema é um **cadastro de referência** para o escritório — não um ERP de condomínio. Sem reservas, sem moradores, sem financeiro do condomínio, sem ocorrências, sem fornecedores, sem votação. Só: condomínios, contatos (síndico/subsíndico/conselheiro), funcionários vinculados, áreas comuns, assembleias e avisos/alertas com documentos anexados.
**Stack:** Java puro (JDBC) + `HttpServer` do JDK · PostgreSQL 16 (1 database por empresa) · Frontend HTML/CSS/JS puro no mesmo processo · Docker Compose.
---
## 1. Diagrama — `db_master` (sem mudança desde v2.0)
```mermaid
erDiagram
EMPRESAS ||--o{ FUNCIONARIOS : tem
EMPRESAS ||--o{ API_KEYS : possui
EMPRESAS ||--o{ EMPRESA_FUNCIONALIDADES : libera
FUNCIONALIDADES ||--o{ EMPRESA_FUNCIONALIDADES : referenciada_em
EMPRESAS ||--o{ EVOLUTION_INSTANCIAS : cadastra
PLANOS ||--o{ PLANO_FUNCIONALIDADES : inclui
FUNCIONALIDADES ||--o{ PLANO_FUNCIONALIDADES : referenciada_em
PLANOS ||--o{ ASSINATURAS : usado_em
EMPRESAS ||--o{ ASSINATURAS : possui
EMPRESAS ||--o{ PERIODOS_USO : tem
PERIODOS_USO ||--o{ PAGAMENTOS : gera
EMPRESAS ||--o{ LOGS_ENVIO_AVISOS : gera
EMPRESAS ||--o{ LEMBRETES_COBRANCA : recebe
```
Schema igual ao documento v2.0 (empresas, funcionarios, api_keys, funcionalidades, empresa_funcionalidades, evolution_instancias, configuracoes_plataforma, planos, plano_funcionalidades, assinaturas, periodos_uso, pagamentos, logs_envio_avisos, lembretes_cobranca) — nada disso mudou.
---
## 2. Diagrama — `db_empresa_<slug>` (NOVO, bem mais simples)
```mermaid
erDiagram
CONDOMINIOS ||--o{ CONTATO_CONDOMINIO : vincula
CONTATOS ||--o{ CONTATO_CONDOMINIO : vinculado_a
CONDOMINIOS ||--o{ FUNCIONARIO_CONDOMINIO : atendido_por
CONDOMINIOS ||--o{ ESPACOS_COMUNS : possui
CONDOMINIOS ||--o{ ASSEMBLEIAS : realiza
CONDOMINIOS ||--o{ AVISOS : publica
ASSEMBLEIAS ||--o{ AVISOS : gera
AVISOS ||--o{ AVISO_DOCUMENTOS : anexa
DOCUMENTOS ||--o{ AVISO_DOCUMENTOS : anexado_em
AVISOS ||--o{ AVISO_DESTINATARIOS : enviado_para
CONDOMINIOS ||--o{ DOCUMENTOS : arquiva
```
### Schema
```sql
CREATE TABLE condominios (
id BIGSERIAL PRIMARY KEY,
nome VARCHAR(150) NOT NULL,
cnpj VARCHAR(18) UNIQUE,
endereco VARCHAR(255),
criado_em TIMESTAMP NOT NULL DEFAULT now()
);
-- pessoa física (síndico/subsíndico/conselheiro) — cadastrada uma vez,
-- pode ser vinculada a mais de um condomínio
CREATE TABLE contatos (
id BIGSERIAL PRIMARY KEY,
nome VARCHAR(150) NOT NULL,
telefone VARCHAR(20), -- WhatsApp
email VARCHAR(150),
criado_em TIMESTAMP NOT NULL DEFAULT now()
);
-- vínculo N:N entre contato e condomínio, com o papel que exerce ali
CREATE TABLE contato_condominio (
contato_id BIGINT NOT NULL REFERENCES contatos(id),
condominio_id BIGINT NOT NULL REFERENCES condominios(id),
tipo VARCHAR(20) NOT NULL, -- sindico, subsindico, conselheiro
data_inicio_mandato DATE,
data_fim_mandato DATE,
status VARCHAR(20) NOT NULL DEFAULT 'ativo',
PRIMARY KEY (contato_id, condominio_id, tipo)
);
-- "síndico atual" de um condomínio = SELECT ... WHERE condominio_id=X AND tipo='sindico' AND status='ativo'
-- funcionário do escritório (cadastro fica em db_master.funcionarios) vinculado
-- a quais condomínios ele cuida — também recebe avisos/alertas desses condomínios
CREATE TABLE funcionario_condominio (
funcionario_id BIGINT NOT NULL, -- referência a db_master.funcionarios, sem FK cross-db
condominio_id BIGINT NOT NULL REFERENCES condominios(id),
PRIMARY KEY (funcionario_id, condominio_id)
);
CREATE TABLE espacos_comuns (
id BIGSERIAL PRIMARY KEY,
condominio_id BIGINT NOT NULL REFERENCES condominios(id),
nome VARCHAR(100) NOT NULL, -- salão de festas, churrasqueira, quadra...
descricao TEXT,
valor_utilizacao_centavos BIGINT,
indice_correcao VARCHAR(30), -- IGPM, IPCA, definido em assembleia...
data_ultimo_reajuste DATE,
criado_em TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE assembleias (
id BIGSERIAL PRIMARY KEY,
condominio_id BIGINT NOT NULL REFERENCES condominios(id),
tipo VARCHAR(20) NOT NULL, -- ordinaria, extraordinaria
data_hora TIMESTAMP NOT NULL,
local VARCHAR(150),
pauta TEXT,
status VARCHAR(20) NOT NULL DEFAULT 'agendada', -- agendada, realizada, cancelada
criado_em TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE documentos (
id BIGSERIAL PRIMARY KEY,
condominio_id BIGINT NOT NULL REFERENCES condominios(id),
tipo VARCHAR(40), -- ata, convocacao, regimento, convencao, contrato, outro
nome_arquivo VARCHAR(255) NOT NULL,
caminho_armazenamento VARCHAR(500) NOT NULL,
versao INT NOT NULL DEFAULT 1,
enviado_por BIGINT, -- id em db_master.funcionarios
criado_em TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE avisos (
id BIGSERIAL PRIMARY KEY,
condominio_id BIGINT NOT NULL REFERENCES condominios(id),
assembleia_id BIGINT REFERENCES assembleias(id), -- opcional: aviso sobre uma assembleia específica
tipo VARCHAR(20) NOT NULL DEFAULT 'aviso', -- aviso, alerta
titulo VARCHAR(150) NOT NULL,
mensagem TEXT NOT NULL,
data_evento TIMESTAMP, -- data/hora do evento relacionado (se houver)
data_publicacao TIMESTAMP NOT NULL DEFAULT now(),
data_expiracao TIMESTAMP,
prioridade VARCHAR(10) DEFAULT 'normal', -- baixa, normal, alta, urgente
enviar_whatsapp BOOLEAN NOT NULL DEFAULT false,
enviar_email BOOLEAN NOT NULL DEFAULT false,
status_envio VARCHAR(20) NOT NULL DEFAULT 'nao_enviado', -- nao_enviado, enviado, falhou
criado_por_funcionario BIGINT, -- id em db_master.funcionarios
criado_em TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE aviso_documentos (
aviso_id BIGINT NOT NULL REFERENCES avisos(id),
documento_id BIGINT NOT NULL REFERENCES documentos(id),
PRIMARY KEY (aviso_id, documento_id)
);
-- destinatário pode ser um contato (síndico/subsíndico/conselheiro) OU um funcionário do escritório
CREATE TABLE aviso_destinatarios (
aviso_id BIGINT NOT NULL REFERENCES avisos(id),
tipo_destinatario VARCHAR(20) NOT NULL, -- contato, funcionario
destinatario_id BIGINT NOT NULL, -- contatos.id ou db_master.funcionarios.id, conforme tipo
PRIMARY KEY (aviso_id, tipo_destinatario, destinatario_id)
);
CREATE TABLE log_auditoria (
id BIGSERIAL PRIMARY KEY,
funcionario_id BIGINT,
acao VARCHAR(20) NOT NULL, -- create, update, delete
tabela_afetada VARCHAR(60) NOT NULL,
registro_id BIGINT,
dados_anteriores JSONB,
dados_novos JSONB,
criado_em TIMESTAMP NOT NULL DEFAULT now()
);
```
**Removido em relação à v2.0:** `contatos_condominio` (virou `contatos` + `contato_condominio`), `reservas_area_comum`, `regras_cobranca`, `historico_reajustes`, `prestacoes_contas`, `lancamentos_financeiros`, `ocorrencias`, `fornecedores`, `votacoes`.
---
## 3. Fluxo do exemplo que você deu
"Assembleia dia 23/11/2026 às 18:00 → vincular documentos pertinentes":
1. Cadastra em `assembleias`: `condominio_id`, `tipo='ordinaria'`, `data_hora='2026-11-23 18:00'`, `pauta`.
2. Cria um `avisos` com `assembleia_id` apontando pra ela, `tipo='aviso'`, `data_evento` = mesma data/hora.
3. Anexa os documentos (convocação, pauta detalhada) via `aviso_documentos`.
4. Escolhe destinatários em `aviso_destinatarios` (síndico, subsíndico, conselheiros daquele condomínio + funcionários vinculados) — ou marca "todos" como padrão sugerido.
5. Marca `enviar_whatsapp`/`enviar_email` e dispara.
---
## 4. Autenticação, financeiro/AbacatePay, dashboard, catálogo de funcionalidades
Sem mudanças em relação à v2.0 — seguem valendo:
- 3 camadas de auth (master / escritório / API Key)
- Módulo financeiro do escritório (assinaturas, períodos de uso, pagamentos PIX via AbacatePay, bloqueio automático)
- Menu do dashboard (Master: Clientes/Planos/Financeiro/Configurações · Cliente: Cadastro Condomínio/Financeiro/Configurações)
- Catálogo de `funcionalidades` (whatsapp, e-mail, API, exportação, auditoria etc.)
---
## 5. Endpoints ajustados
```
CRUD /api/condominios
CRUD /api/condominios/{id}/contatos (vincula/desvincula tipo=sindico|subsindico|conselheiro)
CRUD /api/condominios/{id}/funcionarios-vinculados
CRUD /api/condominios/{id}/espacos-comuns
CRUD /api/condominios/{id}/assembleias
CRUD /api/condominios/{id}/documentos
POST /api/condominios/{id}/avisos (body: assembleia_id?, documentos: [id,...], destinatarios: [{tipo, id}, ...])
GET /api/condominios/{id}/avisos/{id}
```
**App externo (API Key)**
```
GET /api/external/condominios/{id}/resumo
→ síndico/subsíndico/conselheiros atuais, próxima assembleia, avisos ativos, espaços comuns
```
---
## 6. Próximos passos
Escopo definitivo fechado. Posso começar os prompt files numerados agora (`01-setup-projeto-docker.md` em diante)?
+115
View File
@@ -0,0 +1,115 @@
# Prompt 01 — Setup do projeto, Docker e HttpServer básico
## Contexto
Início do zero. Projeto: sistema de cadastro de referência para escritórios de contabilidade de condomínio (multi-tenant, 1 database PostgreSQL por empresa + 1 `db_master`). Stack: **Java puro** (sem framework), JDBC puro, `com.sun.net.httpserver.HttpServer` (built-in do JDK) para HTTP, PostgreSQL 16, Docker Compose. Frontend HTML/CSS/JS puro, servido pelo mesmo processo Java.
Este é o primeiro prompt da sequência. Não existe nenhum código ainda.
## Objetivo
Criar o esqueleto do projeto: estrutura de pastas, Dockerfile, docker-compose.yml, conexão JDBC básica com o `db_master`, e um `HttpServer` que sobe, responde a um healthcheck, e já tem a estrutura de roteamento pronta para os próximos prompts registrarem handlers.
## Especificação
### 1. Estrutura de pastas
```
condominio-contabilidade/
├── src/main/java/com/seconectai/condocontab/
│ ├── Main.java
│ ├── config/
│ │ ├── AppConfig.java (lê variáveis de ambiente)
│ │ └── DataSourceProvider.java (cria/gerencia conexões JDBC)
│ ├── http/
│ │ ├── Router.java (registra rotas -> handlers)
│ │ └── StaticFileHandler.java (serve arquivos de webapp/)
│ ├── master/ (vazio por enquanto, prompts futuros)
│ ├── tenant/ (vazio por enquanto)
│ ├── auth/ (vazio por enquanto)
│ ├── integracao/ (vazio por enquanto)
│ └── db/
│ └── MigrationRunner.java (executa .sql de resources/migrations)
├── src/main/resources/
│ └── migrations/
│ ├── master/ (vazio por enquanto — prompt 02)
│ └── tenant/ (vazio por enquanto — prompt 03)
├── src/main/webapp/
│ └── index.html (placeholder: "Sistema no ar")
├── Dockerfile
├── docker-compose.yml
├── .env.example
└── README.md
```
### 2. `AppConfig.java`
Lê das variáveis de ambiente (com valores default sensatos para dev local):
- `DB_HOST`, `DB_PORT` (5432), `DB_USER`, `DB_PASSWORD`, `DB_MASTER_NAME` (db_master)
- `HTTP_PORT` (8080)
- `MASTER_PASSWORD_HASH` (usado só no prompt 04, mas já lido aqui)
- `JWT_SECRET` (usado só no prompt 04)
Falhar de forma clara (log + exit) se uma variável obrigatória estiver ausente.
### 3. `DataSourceProvider.java`
- Sem HikariCP nem pool de terceiros: implementar um pool simples próprio (uma `BlockingQueue<Connection>` com tamanho configurável, ex: 10 conexões) OU usar `DriverManager.getConnection()` direto por enquanto (mais simples) com um método `getMasterConnection()`. Documentar no código que o cache de DataSource por tenant vem no prompt 03/06.
- Testar a conexão na subida da aplicação (`SELECT 1`) e logar sucesso/erro.
### 4. `MigrationRunner.java`
- Lê todos os arquivos `.sql` de um diretório de resources (ordenados por nome, prefixo numérico tipo `001_xxx.sql`, `002_xxx.sql`), executa os que ainda não rodaram.
- Cria uma tabela de controle `schema_migrations (id, nome_arquivo, executado_em)` no banco de destino, se não existir.
- Método `runMigrations(Connection conn, String diretorioResources)` reutilizável tanto para `db_master` quanto para cada `db_empresa_<slug>` (usado nos prompts 02 e 03).
### 5. `Router.java` + `Main.java`
- `HttpServer` escutando em `HTTP_PORT`, com um `Router` que mapeia `path -> handler` (pode ser simples: um `Map<String, HttpHandler>` com suporte a prefixo, tipo `/api/**` cai num handler, senão cai no `StaticFileHandler`).
- Endpoint `GET /health` retornando `200 OK` com `{"status":"ok"}` em JSON puro (sem lib de JSON externa — escrever um helper simples `Json.write(Map<String,Object>)` que serializa tipos básicos; pode ser reaproveitado nos próximos prompts).
- `StaticFileHandler` serve os arquivos de `src/main/webapp/` (ou do diretório equivalente dentro do JAR/imagem) para qualquer rota que não comece com `/api/`.
### 6. Dockerfile (multi-stage)
- Stage 1: build com `maven:3.9-eclipse-temurin-21` (ou Gradle, se preferir — usar Maven por simplicidade), gera o JAR.
- Stage 2: `eclipse-temurin:21-jre`, copia o JAR + a pasta `webapp/`, expõe `8080`, `ENTRYPOINT ["java","-jar","app.jar"]`.
### 7. docker-compose.yml
```yaml
version: "3.9"
services:
postgres:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: db_master
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "5432:5432"
app:
build: .
environment:
DB_HOST: postgres
DB_PORT: 5432
DB_USER: app
DB_PASSWORD: ${DB_PASSWORD}
DB_MASTER_NAME: db_master
HTTP_PORT: 8080
MASTER_PASSWORD_HASH: ${MASTER_PASSWORD_HASH}
JWT_SECRET: ${JWT_SECRET}
depends_on:
- postgres
ports:
- "8080:8080"
volumes:
pgdata:
```
### 8. `.env.example`
Listar todas as variáveis usadas, com comentário de exemplo (sem valores reais sensíveis).
## Critérios de aceite
- [ ] `docker compose up` sobe `postgres` e `app` sem erro.
- [ ] `GET http://localhost:8080/health` retorna `200` com `{"status":"ok"}`.
- [ ] `GET http://localhost:8080/` retorna o `index.html` placeholder.
- [ ] Log de startup mostra "Conexão com db_master OK".
- [ ] `MigrationRunner` roda sem erro mesmo sem nenhum arquivo `.sql` ainda (diretório vazio = no-op).
- [ ] Projeto builda com `mvn clean package` (ou `gradle build`) sem dependências externas além do driver JDBC do PostgreSQL (`org.postgresql:postgresql`).
- [ ] README.md com instruções de como rodar localmente (`docker compose up`, variáveis de `.env`).
## Ao concluir
Atualizar `10-tracker-progresso.md` marcando o Prompt 01 como concluído, com a data e um resumo de 1-2 linhas do que foi feito. Não avançar para o Prompt 02 sem esse tracker atualizado.
+82
View File
@@ -0,0 +1,82 @@
# Prompt 02 — Migrations do `db_master`
## Contexto
O Prompt 01 já entregou: projeto Java rodando em Docker, `HttpServer` no ar, `MigrationRunner` pronto pra rodar arquivos `.sql`, `DataSourceProvider` com conexão ao `db_master`. **Leia o tracker (`10-tracker-progresso.md`) antes de começar** para confirmar que o Prompt 01 está marcado como concluído.
Este prompt cria todo o schema do banco de catálogo (`db_master`): empresas, funcionários, planos, funcionalidades, financeiro (assinaturas/pagamentos), logs.
## Objetivo
Criar os arquivos de migration SQL em `src/main/resources/migrations/master/`, rodá-los automaticamente na subida da aplicação (se `db_master` ainda não tiver as tabelas), e criar as classes Java de acesso a dados (DAO) básicas para essas tabelas — sem endpoints HTTP ainda (isso vem no Prompt 05).
## Especificação
### 1. Migrations (uma tabela por arquivo, numeração sequencial)
Criar em `src/main/resources/migrations/master/`:
- `001_empresas.sql`
- `002_funcionarios.sql`
- `003_api_keys.sql`
- `004_funcionalidades.sql`
- `005_empresa_funcionalidades.sql`
- `006_evolution_instancias.sql`
- `007_configuracoes_plataforma.sql`
- `008_planos.sql`
- `009_plano_funcionalidades.sql`
- `010_assinaturas.sql`
- `011_periodos_uso.sql`
- `012_pagamentos.sql`
- `013_logs_envio_avisos.sql`
- `014_lembretes_cobranca.sql`
- `015_seed_funcionalidades.sql` (INSERT do catálogo — ver abaixo)
Usar exatamente o DDL do documento de arquitetura v3.0 (seção 1), tabela por tabela — não inventar colunas novas. Reproduzo aqui as tabelas que essa fase cria (copie fielmente, incluindo tipos e defaults):
- `empresas(id, nome, cnpj, slug, database_name, status, criado_em)`
- `funcionarios(id, empresa_id, nome, email, senha_hash, cargo, status, criado_em)`
- `api_keys(id, empresa_id, key_hash, descricao, ativa, criado_em, revogada_em)`
- `funcionalidades(id, chave, nome, descricao)`
- `empresa_funcionalidades(empresa_id, funcionalidade_id, ativa, liberado_em, liberado_por)` — PK composta
- `evolution_instancias(id, empresa_id, instance_url, instance_token_enc, numero_whatsapp, status, solicitado_em, aprovado_em, aprovado_por)`
- `configuracoes_plataforma(chave, valor, atualizado_em)` — PK = chave
- `planos(id, nome, valor_mensal_centavos, descricao, ativo, criado_em)`
- `plano_funcionalidades(plano_id, funcionalidade_id)` — PK composta
- `assinaturas(id, empresa_id, plano_id, valor_mensal_centavos, dia_vencimento, status, criado_em)`
- `periodos_uso(id, empresa_id, mes_referencia, status, data_liberacao, data_bloqueio)` — UNIQUE(empresa_id, mes_referencia)
- `pagamentos(id, empresa_id, periodo_uso_id, valor_centavos, abacatepay_charge_id, qr_code_payload, qr_code_base64, status, criado_em, pago_em)`
- `logs_envio_avisos(id, empresa_id, condominio_id, aviso_id, contato_id, contato_nome, canal, status, erro_detalhe, criado_em)`
- `lembretes_cobranca(id, empresa_id, periodo_uso_id, canal, enviado_por, criado_em)`
Seed (`015_seed_funcionalidades.sql`): usar o catálogo completo (MVP + Fase 2) da seção 6 do documento de arquitetura v3.0 — 24 chaves MVP + 6 chaves Fase 2. Copiar os `INSERT INTO funcionalidades` literalmente.
### 2. Índices recomendados
Adicionar (na própria migration de cada tabela, ou em um `016_indices.sql`):
```sql
CREATE INDEX idx_funcionarios_empresa ON funcionarios(empresa_id);
CREATE INDEX idx_api_keys_empresa ON api_keys(empresa_id);
CREATE INDEX idx_evolution_empresa ON evolution_instancias(empresa_id);
CREATE INDEX idx_assinaturas_empresa ON assinaturas(empresa_id);
CREATE INDEX idx_periodos_uso_empresa ON periodos_uso(empresa_id);
CREATE INDEX idx_pagamentos_empresa ON pagamentos(empresa_id);
CREATE INDEX idx_pagamentos_periodo ON pagamentos(periodo_uso_id);
CREATE INDEX idx_logs_envio_empresa ON logs_envio_avisos(empresa_id, status);
```
### 3. Rodar migrations na subida
No `Main.java`, após confirmar conexão com `db_master`, chamar `MigrationRunner.runMigrations(masterConnection, "migrations/master")`.
### 4. DAOs básicos (sem endpoints ainda)
Criar em `com.seconectai.condocontab.master.dao`:
- `EmpresaDao` (métodos: `inserir`, `buscarPorId`, `listarTodas`, `atualizarStatus`)
- `FuncionarioDao` (`inserir`, `buscarPorEmail`, `listarPorEmpresa`)
- `FuncionalidadeDao` (`listarTodas`, `buscarPorChave`)
Usar JDBC puro (`PreparedStatement`), sem ORM. Todo valor monetário como `long` (centavos) no Java.
## Critérios de aceite
- [ ] `docker compose up` roda as 15 migrations em ordem, sem erro, na primeira subida.
- [ ] Subir o container uma segunda vez NÃO tenta re-executar migrations já aplicadas (tabela `schema_migrations` funcionando).
- [ ] `SELECT * FROM funcionalidades` retorna as 30 linhas do seed (24 MVP + 6 Fase 2).
- [ ] `EmpresaDao.inserir` + `EmpresaDao.buscarPorId` funcionam via um teste manual (pode ser um `main` temporário ou teste JUnit simples).
- [ ] Nenhuma tabela usa `DELETE` como estratégia de remoção — todas têm campo de `status` para inativação lógica onde fizer sentido (já está no DDL).
## Ao concluir
Atualizar `10-tracker-progresso.md`: Prompt 02 concluído, data, resumo. Confirmar que Prompt 01 segue marcado como concluído antes de finalizar.
+67
View File
@@ -0,0 +1,67 @@
# Prompt 03 — Migrations do `db_empresa_<slug>` (tenant) e criação dinâmica de database
## Contexto
Prompts 01 e 02 concluídos: projeto no ar, `db_master` com schema completo. **Leia `10-tracker-progresso.md` antes de começar.**
Este prompt cria: (a) o schema que roda dentro de cada `db_empresa_<slug>`, (b) o mecanismo que cria um novo database Postgres + roda essas migrations quando uma empresa nova é cadastrada, e (c) o cache de conexões por tenant.
## Objetivo
Implementar `TenantDataSourceProvider` (cria/reaproveita conexão por `empresa_id`) e `TenantProvisioner` (cria o database físico + roda migrations quando uma empresa é criada), e as migrations do schema operacional (condomínios, contatos, avisos etc. da arquitetura v3.0).
## Especificação
### 1. Migrations — `src/main/resources/migrations/tenant/`
Criar, na ordem, usando o DDL exato da seção 2 do documento de arquitetura v3.0:
- `001_condominios.sql`
- `002_contatos.sql`
- `003_contato_condominio.sql`
- `004_funcionario_condominio.sql`
- `005_espacos_comuns.sql`
- `006_assembleias.sql`
- `007_documentos.sql`
- `008_avisos.sql`
- `009_aviso_documentos.sql`
- `010_aviso_destinatarios.sql`
- `011_log_auditoria.sql`
- `012_indices.sql`:
```sql
CREATE INDEX idx_contato_condominio_condominio ON contato_condominio(condominio_id);
CREATE INDEX idx_contato_condominio_contato ON contato_condominio(contato_id);
CREATE INDEX idx_funcionario_condominio_condominio ON funcionario_condominio(condominio_id);
CREATE INDEX idx_espacos_condominio ON espacos_comuns(condominio_id);
CREATE INDEX idx_assembleias_condominio ON assembleias(condominio_id, data_hora);
CREATE INDEX idx_documentos_condominio ON documentos(condominio_id);
CREATE INDEX idx_avisos_condominio ON avisos(condominio_id, data_publicacao);
CREATE INDEX idx_avisos_assembleia ON avisos(assembleia_id);
CREATE INDEX idx_aviso_destinatarios_aviso ON aviso_destinatarios(aviso_id);
```
**Importante:** confira o DDL contra o documento v3.0 antes de escrever — `avisos.assembleia_id` é opcional (nullable), `aviso_destinatarios.destinatario_id` NÃO tem FK (referencia `contatos.id` OU `db_master.funcionarios.id` dependendo de `tipo_destinatario`), `funcionario_condominio.funcionario_id` também não tem FK (cross-database).
### 2. `TenantProvisioner`
Classe responsável por, dado um `nome` e `slug` de empresa:
1. Conectar no Postgres com um usuário administrativo (não o `app` comum) e rodar `CREATE DATABASE db_empresa_<slug>` (sanitizar o slug: só `[a-z0-9_]`, gerar erro claro se inválido).
2. Abrir conexão no novo database recém-criado.
3. Rodar `MigrationRunner.runMigrations(conn, "migrations/tenant")` nele.
4. Retornar sucesso/erro de forma que o chamador (Prompt 05, ao cadastrar empresa) possa tratar — se falhar no meio, o ideal é logar claramente e deixar o registro em `empresas.status` como algo como `provisionamento_pendente` (pode adicionar esse status à coluna livremente, é `VARCHAR`).
### 3. `TenantDataSourceProvider`
- Um `Map<Long empresaId, Connection>` (ou pool simples por tenant) em memória, populado sob demanda: na primeira requisição que precisa do tenant X, abre a conexão em `db_empresa_<database_name>` (buscando `database_name` na tabela `empresas` do master) e guarda no cache.
- Método `getConnection(long empresaId)` — se não estiver no cache, busca `database_name` no master, conecta, guarda, retorna.
- Não precisa de invalidação de cache sofisticada nesta fase — documentar como ponto de atenção futuro (ex: quando uma empresa é cancelada/bloqueada, talvez fechar a conexão do cache; deixar um método `invalidar(empresaId)` já pronto, mesmo que não seja chamado ainda).
### 4. DAOs básicos do tenant (sem endpoints ainda — vêm no Prompt 06)
Criar em `com.seconectai.condocontab.tenant.dao`, todos recebendo a `Connection` do tenant como parâmetro (não guardam estado):
- `CondominioDao` (`inserir`, `buscarPorId`, `listarTodos`)
- `ContatoDao` (`inserir`, `buscarPorId`)
- `ContatoCondominioDao` (`vincular`, `listarPorCondominio(condominioId)`, `buscarSindicoAtual(condominioId)` — query com `WHERE tipo='sindico' AND status='ativo'`)
## Critérios de aceite
- [ ] Rodar `TenantProvisioner.provisionar("Empresa Teste", "empresa-teste")` cria de fato um novo database `db_empresa_empresa_teste` no Postgres, com as 11 tabelas do schema tenant.
- [ ] Rodar de novo para outro slug cria outro database, isolado do primeiro (inserir um condomínio em um não aparece no outro).
- [ ] `TenantDataSourceProvider.getConnection(empresaId)` retorna conexão válida e reaproveita a mesma conexão em chamadas subsequentes (validar com log ou teste).
- [ ] Slug inválido (ex: com espaço ou maiúscula) é rejeitado com mensagem de erro clara, sem tentar criar o database.
- [ ] `ContatoCondominioDao.buscarSindicoAtual` retorna `null`/vazio quando não há síndico ativo, sem lançar exceção.
## Ao concluir
Atualizar `10-tracker-progresso.md`: Prompt 03 concluído, data, resumo. Confirmar Prompts 01 e 02 seguem marcados.
+60
View File
@@ -0,0 +1,60 @@
# Prompt 04 — Autenticação: senha master, login de funcionário (JWT) e API Key
## Contexto
Prompts 01–03 concluídos: app no ar, `db_master` e `db_empresa` com schema completo, DAOs básicos, provisionamento de tenant funcionando. **Leia `10-tracker-progresso.md` antes de começar.**
Este prompt implementa as 3 camadas de autenticação da arquitetura v3.0: master (senha única), funcionário do escritório (JWT), app externo (API Key).
## Objetivo
Implementar geração/validação de JWT (sem lib pesada — implementar HMAC-SHA256 manualmente ou usar uma lib mínima tipo `java-jwt`/`jjwt`, only se realmente necessário; preferir implementação própria já que o projeto é "Java puro"), hashing de senha (BCrypt — aqui vale trazer uma lib pequena e específica, tipo `at.favre.lib:bcrypt`, já que reimplementar bcrypt do zero não compensa), geração/validação de API Key, e os middlewares de autenticação usados pelos handlers HTTP dos próximos prompts.
## Especificação
### 1. Hash de senha
- Usar BCrypt (biblioteca `at.favre.lib:bcrypt`, única dependência externa além do driver JDBC e (se precisar) da lib de JWT).
- `PasswordHasher.hash(senha)` e `PasswordHasher.verificar(senha, hash)`.
### 2. JWT
- `JwtService.gerar(Map<String,Object> claims, long validadeSegundos)` → string do token.
- `JwtService.validar(String token)` → `Optional<Map<String,Object>> claims` (vazio se inválido/expirado).
- Claims para o token **master**: `{"escopo": "master"}`.
- Claims para o token de **funcionário**: `{"escopo": "empresa", "empresa_id": X, "funcionario_id": Y, "cargo": "..."}`.
- Assinatura HMAC-SHA256 usando `JWT_SECRET` (variável de ambiente já lida no Prompt 01).
- Validade sugerida: 8 horas para funcionário, 2 horas para master (reautenticação frequente por segurança, já que é a chave mestra do sistema).
### 3. API Key
- Geração: `ApiKeyService.gerar()` → retorna a chave em texto puro (mostrada **uma única vez** ao usuário) + o hash (SHA-256 é suficiente aqui, não precisa de bcrypt) que é salvo em `api_keys.key_hash`.
- Validação: `ApiKeyService.validar(String chaveRecebida)` → busca por hash no banco master, confere `ativa=true`, retorna o `empresa_id` vinculado (ou vazio se inválida/revogada).
### 4. Endpoints
```
POST /api/master/login body: {"senha": "..."}
→ confere hash contra MASTER_PASSWORD_HASH (env var); se ok, retorna {"token": "..."}
POST /api/auth/login body: {"email": "...", "senha": "..."}
→ busca funcionario por email (em qual empresa? funcionarios.email é único globalmente,
então a busca não precisa de empresa_id no body), confere senha, gera JWT de empresa
→ se funcionarios.status != 'ativo' OU empresas.status != 'ativa': erro 403 com mensagem clara
("cadastro desativado" ou "escritório com pendência financeira", respectivamente)
```
### 5. Middlewares (para uso nos próximos prompts)
Criar filtros/handlers wrapper reutilizáveis:
- `exigirMaster(HttpHandler)` — lê header `Authorization: Bearer <token>`, valida escopo=master, senão retorna 401.
- `exigirFuncionario(HttpHandler)` — idem, escopo=empresa; injeta `empresa_id`/`funcionario_id`/`cargo` no contexto da requisição (pode ser via `HttpExchange.setAttribute` ou um `ThreadLocal` simples).
- `exigirApiKey(HttpHandler)` — lê header `X-API-Key`, valida, injeta `empresa_id` no contexto.
### 6. Segurança básica
- Nunca logar senha, token completo ou API key em texto puro nos logs (logar no máximo os primeiros 6 caracteres + "...").
- Rate limiting simples no `/api/master/login` e `/api/auth/login` (ex: máximo 5 tentativas por IP a cada 5 minutos, em memória — não precisa de Redis nesta fase).
## Critérios de aceite
- [ ] `POST /api/master/login` com senha errada retorna 401; com senha certa retorna um JWT válido.
- [ ] Token master usado em `exigirFuncionario` é rejeitado (escopo errado) — e vice-versa.
- [ ] `POST /api/auth/login` com funcionário de empresa bloqueada (`empresas.status='bloqueada'`) retorna 403 com mensagem específica de pendência financeira.
- [ ] API Key gerada, salva com hash, validada corretamente; uma chave revogada (`ativa=false`) é rejeitada.
- [ ] 6ª tentativa de login em 5 minutos do mesmo IP é bloqueada temporariamente.
- [ ] Nenhum log do sistema imprime senha, token completo ou API key completa.
## Ao concluir
Atualizar `10-tracker-progresso.md`: Prompt 04 concluído, data, resumo. Confirmar Prompts 01–03 seguem marcados.
+86
View File
@@ -0,0 +1,86 @@
# Prompt 05 — Painel Master: clientes, planos e funcionalidades
## Contexto
Prompts 01–04 concluídos: schema completo (master + tenant), provisionamento de tenant, autenticação (master/funcionário/API Key) funcionando. **Leia `10-tracker-progresso.md` antes de começar.**
Este prompt implementa os endpoints do menu **Master → Clientes** e **Master → Planos**, incluindo o provisionamento de empresa nova (usa o `TenantProvisioner` do Prompt 03) e o toggle de funcionalidades por empresa.
## Objetivo
CRUD de empresas (clientes), CRUD de planos, ativação/desativação de funcionalidades por empresa, listagem de logs de envio com erro e listagem de clientes pagantes/inadimplentes.
Todos os endpoints deste prompt exigem `exigirMaster` (Prompt 04).
## Especificação
### 1. Clientes (empresas)
```
POST /api/master/empresas
body: {"nome": "...", "cnpj": "...", "slug": "..."}
→ valida slug único, cria registro em `empresas` (status inicial: 'provisionando'),
chama TenantProvisioner de forma síncrona (aceitável nesta fase — trocar por fila
assíncrona é uma melhoria futura, não bloquear o prompt por isso),
atualiza status para 'ativa' ao concluir (ou 'provisionamento_pendente' se falhar)
→ retorna o registro criado, incluindo se o provisionamento deu certo
GET /api/master/empresas
query params opcionais: ?status=ativa|bloqueada|cancelada
→ lista com nome, status, plano vinculado (join com assinaturas+planos), situação do
período de uso vigente (pago/pendente/bloqueado)
GET /api/master/empresas/{id}
PUT /api/master/empresas/{id} (editar nome/cnpj — NÃO permite editar slug/database_name)
```
### 2. Funcionários da empresa (cadastro pelo master também, já que "cadastro da empresa é feito pelo master")
```
POST /api/master/empresas/{id}/funcionarios
body: {"nome", "email", "senha" (texto puro, só nesta chamada — vira hash), "cargo"}
GET /api/master/empresas/{id}/funcionarios
```
*(Nota: o próprio escritório pode ter uma tela de gestão de funcionários mais pra frente, mas o cadastro inicial do primeiro funcionário é feito pelo master junto com a empresa, senão ninguém consegue logar.)*
### 3. Funcionalidades
```
GET /api/master/empresas/{id}/funcionalidades
→ lista todas as `funcionalidades` com um booleano indicando se está ativa pra essa empresa
POST /api/master/empresas/{id}/funcionalidades/{chave}/ativar
POST /api/master/empresas/{id}/funcionalidades/{chave}/desativar
→ grava em empresa_funcionalidades (liberado_em=now(), liberado_por=<funcionario_id do
token master, se aplicável, senão null>)
```
Ao criar uma empresa vinculada a um plano (Prompt seguinte trata a criação da assinatura), as funcionalidades de `plano_funcionalidades` daquele plano devem ser copiadas automaticamente como "ativas" em `empresa_funcionalidades` — implementar esse copy aqui mesmo, no momento da criação da empresa, SE o body de criação já incluir um `plano_id` opcional.
### 4. Planos
```
GET /api/master/planos
POST /api/master/planos body: {"nome", "valor_mensal_centavos", "descricao", "funcionalidades": ["chave1","chave2"]}
PUT /api/master/planos/{id}
```
Ao criar/editar um plano com a lista de `funcionalidades`, sincronizar `plano_funcionalidades` (remover as que saíram, inserir as novas).
### 5. Logs de envio e clientes pagantes
```
GET /api/master/logs-envio?status=falhou&empresa_id=X (empresa_id opcional)
→ lista de logs_envio_avisos, mais recentes primeiro, paginado (limit/offset)
GET /api/master/empresas?situacao=pagante
→ reaproveita o GET /api/master/empresas, com filtro adicional: join em periodos_uso
do mês corrente, status='pago'
POST /api/master/empresas/{id}/lembrete-cobranca
body: {"canal": "whatsapp"|"email"}
→ grava em lembretes_cobranca; o ENVIO de fato (mensagem real) só é implementado no
Prompt 08, quando a integração Evolution/e-mail existir — por enquanto, só registra
a intenção e retorna sucesso (deixar um TODO claro no código apontando pro Prompt 08)
```
## Critérios de aceite
- [ ] Criar uma empresa via `POST /api/master/empresas` de fato cria o database `db_empresa_<slug>` (reaproveitando o Prompt 03) e um primeiro funcionário consegue logar em seguida via `POST /api/auth/login`.
- [ ] Criar empresa com slug já existente retorna erro 409, sem tentar criar database duplicado.
- [ ] Ativar/desativar funcionalidade reflete corretamente em `GET /api/master/empresas/{id}/funcionalidades`.
- [ ] Criar um plano com 3 funcionalidades e depois criar uma empresa apontando pra esse plano faz as 3 aparecerem já ativas em `empresa_funcionalidades`.
- [ ] `GET /api/master/logs-envio?status=falhou` funciona mesmo sem nenhum log ainda (retorna lista vazia, não erro).
- [ ] Todos os endpoints deste prompt retornam 401 se chamados sem token master válido.
## Ao concluir
Atualizar `10-tracker-progresso.md`: Prompt 05 concluído, data, resumo. Confirmar Prompts 01–04 seguem marcados.
+93
View File
@@ -0,0 +1,93 @@
# Prompt 06 — Painel Cliente (escritório): condomínios, contatos, espaços comuns, assembleias, documentos
## Contexto
Prompts 01–05 concluídos: autenticação completa, painel master funcionando, empresas sendo provisionadas com tenant próprio. **Leia `10-tracker-progresso.md` antes de começar.**
Este prompt implementa o CRUD principal do menu **Cliente → Cadastro Condomínio**, que é o coração do sistema: condomínios, contatos (síndico/subsíndico/conselheiro), vínculo de funcionários, espaços comuns, assembleias e documentos.
Todos os endpoints deste prompt exigem `exigirFuncionario` (Prompt 04) e operam sobre o `db_empresa_<slug>` do `empresa_id` do token (usar `TenantDataSourceProvider` do Prompt 03).
## Objetivo
CRUD completo de `condominios`, `contatos` + `contato_condominio`, `funcionario_condominio`, `espacos_comuns`, `assembleias`, `documentos` (upload de arquivo).
## Especificação
### 1. Condomínios
```
POST /api/condominios body: {"nome", "cnpj", "endereco"}
GET /api/condominios lista todos do tenant
GET /api/condominios/{id}
→ resposta agregada: dados do condomínio + síndico atual (via ContatoCondominioDao) +
contagem de assembleias futuras + contagem de avisos ativos (isso é o "checklist"
que o funcionário usa como referência rápida)
PUT /api/condominios/{id}
```
### 2. Contatos (síndico/subsíndico/conselheiro)
```
POST /api/contatos body: {"nome", "telefone", "email"}
→ cria a pessoa (sem vínculo ainda)
POST /api/condominios/{id}/contatos/vincular
body: {"contato_id" (ou dados de um contato novo pra criar+vincular numa chamada só),
"tipo": "sindico"|"subsindico"|"conselheiro",
"data_inicio_mandato", "data_fim_mandato"}
→ grava em contato_condominio; se marcar tipo=sindico e já existir outro síndico ativo
pro mesmo condomínio, perguntar/forçar: por padrão, encerrar automaticamente o mandato
anterior (status='inativo', data_fim_mandato=hoje) e ativar o novo — deixar isso
documentado no código como decisão de negócio
GET /api/condominios/{id}/contatos lista todos os vínculos ativos daquele condomínio
DELETE /api/condominios/{id}/contatos/{contatoId}?tipo=... (desvincula — não apaga o contato,
só remove o vínculo daquele condomínio/tipo específico; um contato pode continuar
vinculado a outros condomínios)
```
### 3. Funcionários vinculados a condomínios
```
POST /api/condominios/{id}/funcionarios-vinculados
body: {"funcionario_id"} (funcionario precisa existir em db_master, mesma empresa)
GET /api/condominios/{id}/funcionarios-vinculados
DELETE /api/condominios/{id}/funcionarios-vinculados/{funcionarioId}
```
### 4. Espaços comuns
```
POST /api/condominios/{id}/espacos-comuns
body: {"nome", "descricao", "valor_utilizacao_centavos", "indice_correcao"}
GET /api/condominios/{id}/espacos-comuns
PUT /api/condominios/{id}/espacos-comuns/{espacoId}
→ se o valor mudar, atualizar data_ultimo_reajuste=hoje automaticamente
```
### 5. Assembleias
```
POST /api/condominios/{id}/assembleias
body: {"tipo", "data_hora", "local", "pauta"}
GET /api/condominios/{id}/assembleias (ordenar por data_hora desc)
PUT /api/condominios/{id}/assembleias/{id} (inclui mudar status: agendada/realizada/cancelada)
```
### 6. Documentos (upload)
```
POST /api/condominios/{id}/documentos multipart/form-data: arquivo + tipo
→ salvar o arquivo em um diretório local montado como volume Docker (ex: /app/uploads/
<empresa_slug>/<condominio_id>/<uuid>-<nome_original>), gravar caminho_armazenamento
com esse path relativo
→ validar extensão (permitir: pdf, jpg, jpeg, png, docx) e tamanho máximo (ex: 10MB)
GET /api/condominios/{id}/documentos
GET /api/documentos/{id}/download → serve o arquivo (Content-Disposition: attachment)
```
Adicionar o volume de uploads no `docker-compose.yml` do Prompt 01 (`./uploads:/app/uploads`).
### 7. Auditoria
Toda operação de `POST`/`PUT`/`DELETE` deste prompt deve gravar uma linha em `log_auditoria` (tabela_afetada, registro_id, ação, dados_anteriores/dados_novos em JSON, funcionario_id do token).
## Critérios de aceite
- [ ] Criar um condomínio, vincular um síndico e um subsíndico, e chamar `GET /api/condominios/{id}` mostra o síndico atual corretamente.
- [ ] Vincular um segundo síndico ao mesmo condomínio encerra automaticamente o mandato do primeiro (`status='inativo'`).
- [ ] O mesmo `contato` pode ser vinculado como síndico em dois condomínios diferentes simultaneamente.
- [ ] Upload de um PDF funciona; upload de um `.exe` é rejeitado com erro claro; upload de 15MB é rejeitado.
- [ ] `log_auditoria` recebe uma linha para cada operação de escrita feita neste prompt (testar com pelo menos 3 operações diferentes).
- [ ] Todos os endpoints só retornam/alteram dados do `empresa_id` do token — tentar acessar `/api/condominios/{id}` de outra empresa retorna 404 (não 403 — não revelar que o ID existe em outro tenant).
## Ao concluir
Atualizar `10-tracker-progresso.md`: Prompt 06 concluído, data, resumo. Confirmar Prompts 01–05 seguem marcados.
+63
View File
@@ -0,0 +1,63 @@
# Prompt 07 — Financeiro: assinaturas, período de uso e AbacatePay
## Contexto
Prompts 01–06 concluídos: CRUD completo do escritório funcionando. **Leia `10-tracker-progresso.md` antes de começar.**
Este prompt implementa o menu **Cliente → Financeiro** (pagar mensalidade) e a automação de bloqueio/liberação de acesso conforme pagamento, usando AbacatePay para gerar cobrança PIX.
## Objetivo
Criar/consultar `periodos_uso` do mês corrente, gerar cobrança PIX via AbacatePay, processar o webhook de confirmação, e rodar o job diário que bloqueia empresas inadimplentes.
## Especificação
### 1. Cliente AbacatePay
Consultar a documentação oficial da AbacatePay (https://docs.abacatepay.com) para confirmar o formato exato do endpoint de criação de cobrança PIX e do payload do webhook — **não assumir o formato sem checar**, a API pode ter mudado. Implementar `AbacatePayClient`:
- `criarCobranca(valorCentavos, descricao, identificadorExterno)` → retorna `{chargeId, qrCodePayload, qrCodeBase64}`
- Validação de assinatura do webhook, se a AbacatePay oferecer esse mecanismo (verificar na doc).
### 2. Endpoints do escritório
```
GET /api/financeiro/periodo-atual
→ busca (ou cria, se não existir) o periodos_uso do mes_referencia = mês corrente;
retorna status, valor da assinatura vigente, se já tem pagamento pendente/gerado
POST /api/financeiro/pagamentos
→ se já existe um pagamento 'pendente' pro período atual, retorna ele (não duplica
cobrança); senão, chama AbacatePayClient.criarCobranca com o valor da assinatura,
grava em `pagamentos`, retorna qr_code_base64 + qr_code_payload pro frontend renderizar
GET /api/financeiro/pagamentos/{id}
→ status atual (útil pro frontend fazer polling enquanto o QR Code está na tela)
```
### 3. Webhook
```
POST /api/master/webhooks/abacatepay
→ SEM autenticação de master/funcionário (é a AbacatePay chamando) — validar por
assinatura/segredo do webhook, conforme a documentação da AbacatePay
→ identifica o pagamento pelo abacatepay_charge_id, marca status='pago', pago_em=now()
→ marca periodos_uso.status='pago', data_liberacao=now()
→ marca empresas.status='ativa' (caso estivesse 'bloqueada')
→ idempotente: se o webhook chegar duplicado (mesma cobrança já processada), não fazer
nada de errado, só retornar 200 de novo
```
### 4. Job de bloqueio diário
`BloqueioJob`, rodando uma vez por dia (implementar com um `ScheduledExecutorService` simples dentro da própria aplicação Java — não precisa de cron externo nesta fase):
1. Para cada `empresa` com status='ativa': busca (ou cria) o `periodos_uso` do mês corrente.
2. Se `hoje > dia_vencimento` da assinatura E `periodos_uso.status != 'pago'`: marca `periodos_uso.status='bloqueado'`, `data_bloqueio=now()`, `empresas.status='bloqueada'`.
3. Logar quantas empresas foram bloqueadas na execução.
### 5. Bloqueio de acesso
No middleware `exigirFuncionario` (Prompt 04), adicionar a checagem: se `empresas.status='bloqueada'`, retornar 403 com corpo `{"erro": "pendencia_financeira", "mensagem": "..."}` em vez de deixar passar — mas **sempre permitir** o próprio `GET /api/financeiro/periodo-atual` e `POST /api/financeiro/pagamentos` mesmo bloqueado, senão o escritório não consegue nem pagar pra desbloquear.
## Critérios de aceite
- [ ] `POST /api/financeiro/pagamentos` gera de fato uma cobrança na AbacatePay (testar em ambiente sandbox, se a AbacatePay oferecer) e retorna QR Code válido.
- [ ] Chamar `POST /api/financeiro/pagamentos` duas vezes seguidas não cria duas cobranças, retorna a mesma pendente.
- [ ] Simular o webhook (chamada manual com um payload de teste) marca o período como pago e desbloqueia a empresa.
- [ ] Rodar o webhook duas vezes com o mesmo `charge_id` não causa erro nem duplica nada.
- [ ] `BloqueioJob` rodado manualmente (chamar o método direto, sem esperar o schedule) bloqueia corretamente uma empresa de teste com vencimento no passado e sem pagamento.
- [ ] Empresa bloqueada consegue logar e acessar só `/api/financeiro/**`; qualquer outro endpoint retorna 403 com o erro `pendencia_financeira`.
## Ao concluir
Atualizar `10-tracker-progresso.md`: Prompt 07 concluído, data, resumo. Confirmar Prompts 01–06 seguem marcados.
+87
View File
@@ -0,0 +1,87 @@
# Prompt 08 — Avisos/alertas: Evolution API (WhatsApp), e-mail e destinatários
## Contexto
Prompts 01–07 concluídos: financeiro funcionando, CRUD de condomínios/contatos completo. **Leia `10-tracker-progresso.md` antes de começar.**
Este prompt implementa: conexão da instância WhatsApp (Evolution API) pelo escritório + aprovação pelo master, configuração de SMTP único da plataforma, e o fluxo completo de criação/envio de avisos com documentos anexados e destinatários.
## Objetivo
CRUD de `avisos` + `aviso_documentos` + `aviso_destinatarios`, conexão/aprovação de instância Evolution, configuração de SMTP, envio real de mensagens (WhatsApp e e-mail) respeitando o catálogo de `funcionalidades` liberado por empresa, e gravação em `logs_envio_avisos`.
## Especificação
### 1. Conectar WhatsApp (escritório)
```
POST /api/config/evolution-instancia/conectar
body: {"instance_url", "numero_whatsapp"}
→ cria a instância na Evolution API (consultar a documentação oficial da Evolution API
pra confirmar o endpoint exato de criação de instância e pareamento por QR Code —
não assumir o formato sem checar), grava em evolution_instancias com status='pendente'
(mesmo depois de parear o QR Code com sucesso do lado da Evolution — a ativação pra
USO no sistema depende da aprovação do master)
→ retorna o QR Code (imagem/base64) pro frontend exibir e o funcionário escanear
GET /api/config/evolution-instancia/status
→ status atual: pendente, aprovada, rejeitada, inativa
```
### 2. Aprovação (master)
```
GET /api/master/empresas/{id}/evolution-instancias
POST /api/master/empresas/{id}/evolution-instancias/{instId}/aprovar
POST /api/master/empresas/{id}/evolution-instancias/{instId}/rejeitar
```
### 3. Configuração de e-mail (master, plataforma única)
```
GET /api/master/configuracoes
PUT /api/master/configuracoes
body: {"smtp_host", "smtp_port", "smtp_user", "smtp_password", "smtp_remetente"}
→ salva em configuracoes_plataforma (senha deve ser guardada de forma reversível pra
uso no envio, mas nunca retornada em texto puro no GET — mascarar tipo "••••1234"
se quiser mostrar algo, ou simplesmente omitir o campo senha no GET)
```
### 4. Avisos — CRUD completo
```
POST /api/condominios/{id}/avisos
body: {"assembleia_id" (opcional), "tipo": "aviso"|"alerta", "titulo", "mensagem",
"data_evento" (opcional), "data_expiracao" (opcional), "prioridade",
"documentos": [documentoId, ...], "destinatarios": [{"tipo":"contato","id":X}, ...],
"enviar_whatsapp": bool, "enviar_email": bool}
→ valida que enviar_whatsapp só pode ser true se a empresa tiver a funcionalidade
'whatsapp_envio' ativa E uma evolution_instancia com status='aprovada'; valida que
enviar_email só pode ser true se a empresa tiver 'email_envio' ativa
→ grava o aviso + vínculos em aviso_documentos + aviso_destinatarios
→ se enviar_whatsapp ou enviar_email = true, dispara o envio (ver seção 5) de forma
síncrona nesta fase (fila assíncrona fica como melhoria futura documentada no código)
GET /api/condominios/{id}/avisos
GET /api/condominios/{id}/avisos/{avisoId}
```
### 5. Envio efetivo
`NotificacaoService.enviarAviso(aviso)`:
1. Busca os `aviso_destinatarios`; para cada um, resolve nome/telefone/email:
- se `tipo_destinatario='contato'`: busca em `contatos` (tenant)
- se `tipo_destinatario='funcionario'`: busca em `db_master.funcionarios`
2. Para cada destinatário, se `enviar_whatsapp`: chama `EvolutionApiClient.enviarMensagem(instanceUrl, token, numero, mensagem, anexos)` (consultar doc da Evolution API pro formato exato de envio com anexo).
3. Se `enviar_email`: chama `EmailClient.enviar(smtpConfig, destinatarioEmail, assunto=titulo, corpo=mensagem, anexos=documentos)` — usar `jakarta.mail` (única dependência nova aqui, é a forma padrão de mandar e-mail em Java puro).
4. Para cada tentativa (sucesso ou falha), grava uma linha em `logs_envio_avisos` (no `db_master`, incluindo `contato_nome` como snapshot).
5. Atualiza `avisos.status_envio` = 'enviado' se todas as tentativas relevantes deram certo, 'falhou' se pelo menos uma falhou.
### 6. Reenvio manual
```
POST /api/condominios/{id}/avisos/{avisoId}/reenviar
→ reprocessa só os destinatários cujo último log foi 'falhou'
```
## Critérios de aceite
- [ ] Escritório sem a funcionalidade `whatsapp_envio` ativa recebe erro claro ao tentar criar aviso com `enviar_whatsapp=true`.
- [ ] Conectar uma instância Evolution gera QR Code; aviso criado com `enviar_whatsapp=true` antes da aprovação do master é rejeitado com mensagem clara ("instância pendente de aprovação").
- [ ] Depois do master aprovar a instância, o mesmo aviso consegue ser enviado.
- [ ] Envio por e-mail funciona de ponta a ponta usando um SMTP de teste (ex: Mailtrap ou Gmail com senha de app) configurado via `PUT /api/master/configuracoes`.
- [ ] Um destinatário com telefone/e-mail inválido gera uma linha `falhou` em `logs_envio_avisos` sem quebrar o envio pros demais destinatários.
- [ ] `POST .../reenviar` só reprocessa quem falhou, não reenvia pra quem já recebeu com sucesso.
- [ ] Documento anexado ao aviso (via `aviso_documentos`) é de fato enviado como anexo no WhatsApp/e-mail.
## Ao concluir
Atualizar `10-tracker-progresso.md`: Prompt 08 concluído, data, resumo. Confirmar Prompts 01–07 seguem marcados.
+75
View File
@@ -0,0 +1,75 @@
# Prompt 09 — Frontend base: layout, menus, autenticação e telas principais
## Contexto
Prompts 01–08 concluídos: toda a API funcionando (master, escritório, financeiro, avisos). **Leia `10-tracker-progresso.md` antes de começar.**
Este prompt implementa o frontend estático (HTML/CSS/JS puro, sem build step) descrito na arquitetura v3.0 — servido pelo mesmo `HttpServer` Java via `StaticFileHandler` (Prompt 01).
## Objetivo
Layout base com menu lateral (Master e Cliente), tela de login única (detecta se é master ou funcionário pelo formulário escolhido), e as telas principais consumindo a API via `fetch()`.
## Especificação
### 1. Estrutura de arquivos
```
src/main/webapp/
├── index.html → redireciona pra /login.html se não autenticado
├── login.html → formulário único com abas "Master" / "Escritório"
├── css/
│ └── app.css (layout base: sidebar + conteúdo, sem framework CSS)
├── js/
│ ├── api.js (wrapper de fetch: injeta Authorization: Bearer <token do localStorage>,
│ trata 401 redirecionando pro login, trata 403 pendencia_financeira
│ mostrando um banner de aviso)
│ ├── auth.js (login, logout, leitura do token)
│ └── util.js (formatação de data, moeda em centavos → R$, toasts de sucesso/erro)
├── master/
│ ├── clientes.html + clientes.js
│ ├── planos.html + planos.js
│ ├── financeiro.html + financeiro.js
│ └── configuracoes.html + configuracoes.js
└── cliente/
├── condominios.html + condominios.js (lista + detalhe agregado do Prompt 06)
├── condominio-detalhe.html + condominio-detalhe.js
│ (abas: Contatos / Espaços comuns / Assembleias / Documentos / Avisos)
├── financeiro.html + financeiro.js (mostra QR Code do PIX, faz polling de status)
└── configuracoes.html + configuracoes.js (conectar WhatsApp, mostra QR Code da Evolution)
```
### 2. Layout base (menu lateral)
Reproduzir exatamente a estrutura definida no documento de arquitetura (seção "Dashboard"):
```
MASTER: Clientes | Planos | Financeiro | Configurações
CLIENTE: Cadastro Condomínio | Financeiro | Configurações
```
- Sidebar fixa à esquerda, com o nome da empresa/usuário logado no topo e botão de logout.
- Indicar visualmente o item de menu ativo.
- Responsivo o suficiente pra não quebrar em uma tela de notebook comum (não precisa ser mobile-first).
### 3. Autenticação no frontend
- `login.html`: duas abas. Aba "Master" só pede senha, chama `POST /api/master/login`. Aba "Escritório" pede e-mail+senha, chama `POST /api/auth/login`.
- Token salvo em `localStorage` (`master_token` ou `funcionario_token`, com o escopo junto pra saber pra qual sidebar redirecionar).
- Todas as páginas verificam token válido no carregamento; se ausente/expirado, redireciona pro login.
### 4. Telas — nível de detalhe esperado
Cada tela deve, no mínimo:
- Listar os dados via `GET`, em tabela simples.
- Ter um formulário (modal ou seção inline) para criar/editar, chamando `POST`/`PUT`.
- Mostrar mensagens de erro da API de forma legível (não só "erro 500").
- `condominio-detalhe.html`: a tela mais rica — mostra os dados do condomínio, e nas abas permite cadastrar contato (com seletor de tipo sindico/subsindico/conselheiro), listar/cadastrar espaços comuns, listar/cadastrar assembleias, fazer upload de documento (`<input type="file">`), e criar um aviso selecionando destinatários (checkboxes dos contatos + funcionários vinculados) e documentos já cadastrados pra anexar.
- `cliente/financeiro.html`: mostra status do período atual; se pendente, botão "Gerar cobrança PIX" que chama a API e renderiza o QR Code (`qr_code_base64` como `<img src="data:image/png;base64,...">`) + o código copia-e-cola em um campo com botão "Copiar"; faz polling a cada 5s em `GET /api/financeiro/pagamentos/{id}` até status mudar pra 'pago'.
- `cliente/configuracoes.html`: botão "Conectar WhatsApp" que chama a API e mostra o QR Code da Evolution pra escanear; mostra o status atual (pendente/aprovada/rejeitada).
### 5. Erro de pendência financeira
Se qualquer chamada de API (fora `/api/financeiro/**`) retornar 403 com `erro: "pendencia_financeira"`, `api.js` deve interceptar e mostrar um banner fixo no topo: "Seu acesso está bloqueado por pendência de pagamento — regularize em Financeiro." com link direto pra `cliente/financeiro.html`, sem deixar a tela quebrada.
## Critérios de aceite
- [ ] Login como master e como funcionário funcionam, cada um caindo na sidebar correta.
- [ ] Fluxo completo pela UI: criar condomínio → cadastrar síndico → cadastrar espaço comum → criar assembleia → subir documento → criar aviso vinculando o documento e selecionando o síndico como destinatário.
- [ ] Tela de financeiro do escritório gera e exibe QR Code real (integrado com o Prompt 07).
- [ ] Tela de configurações do escritório exibe QR Code da Evolution (integrado com o Prompt 08).
- [ ] Banner de pendência financeira aparece corretamente quando a empresa está bloqueada, sem quebrar a navegação.
- [ ] Nenhuma tela depende de build step (webpack/vite/npm) — abre direto servido pelo `HttpServer` Java.
## Ao concluir
Atualizar `10-tracker-progresso.md`: Prompt 09 concluído, data, resumo. Confirmar Prompts 01–08 seguem marcados.
+37
View File
@@ -0,0 +1,37 @@
# Tracker de Progresso — Sistema de Contabilidade de Condomínios
> **Regra obrigatória para a IA executora:** antes de iniciar qualquer prompt, leia este arquivo inteiro e confirme que o prompt anterior está marcado como ✅ Concluído. Nunca avance para o próximo prompt com uma fase anterior incompleta ou com critérios de aceite não verificados. Ao terminar um prompt, atualize a linha correspondente aqui (status, data, resumo) antes de considerar a fase encerrada.
| # | Prompt | Status | Data | Resumo |
|---|---|---|---|---|
| 01 | Setup projeto, Docker, HttpServer básico | ✅ Concluído | 2026-09-28 | Projeto Java/Maven criado com Docker, PostgreSQL, HttpServer, healthcheck, arquivos estáticos e MigrationRunner no-op validados. |
| 02 | Migrations `db_master` | ✅ Concluído | 2026-09-28 | Criadas 15 migrations master com seed de 30 funcionalidades e DAOs JDBC básicos validados. |
| 03 | Migrations `db_empresa` (tenant) + provisionamento | ✅ Concluído | 2026-09-28 | Criadas migrations tenant, TenantProvisioner, cache por empresa e DAOs básicos com smoke test validado. |
| 04 | Autenticação: master, JWT, API Key | ✅ Concluído | 2026-09-28 | Implementados BCrypt, JWT HMAC-SHA256, API Key, logins e middlewares com testes de aceite. |
| 05 | Painel Master: clientes e planos | ✅ Concluído | 2026-09-28 | Implementados endpoints master de empresas, funcionários, planos, funcionalidades, logs e lembretes com validação HTTP. |
| 06 | Painel Cliente: condomínios, contatos, espaços, assembleias, documentos | ✅ Concluído | 2026-09-28 | Implementados endpoints cliente para condomínios, contatos, funcionários vinculados, espaços, assembleias, documentos/upload e auditoria. |
| 07 | Financeiro: assinaturas, período de uso, AbacatePay | ✅ Concluído | 2026-09-28 | Implementados período atual, cobrança PIX AbacatePay/mock, webhook idempotente, job de bloqueio e acesso financeiro para inadimplentes. |
| 08 | Avisos/alertas: Evolution API, e-mail, destinatários | ✅ Concluído | 2026-09-28 | Implementados conexão/aprovação Evolution, SMTP master, CRUD/envio de avisos, logs e reenvio de falhas. |
| 09 | Frontend base: layout, menus, telas principais | 🔶 Em andamento | 2026-09-28 | Frontend estático base implementado com login, layout, telas master/cliente, financeiro PIX, WhatsApp e detalhe de condomínio; build Maven validado, pendente teste E2E HTTP. |
Status possíveis: `⬜ Pendente` · `🔶 Em andamento` · `✅ Concluído` · `❌ Bloqueado` (se bloqueado, escrever o motivo no resumo).
## Decisões de arquitetura já fechadas (não reabrir sem necessidade real)
- Java puro + JDBC, sem framework; `com.sun.net.httpserver.HttpServer` para HTTP.
- PostgreSQL 16, 1 servidor, 1 database por empresa (`db_empresa_<slug>`).
- Frontend HTML/CSS/JS puro, servido pelo mesmo processo (sem build step).
- Autenticação em 3 camadas: senha master → JWT master; funcionário → JWT empresa; app externo → API Key.
- Empresas cadastradas só pelo master (sem autoatendimento).
- Liberação de uso mensal automática via webhook AbacatePay; bloqueio automático por job diário se vencido sem pagamento (dados nunca apagados).
- Evolution API: cada escritório conecta sua própria instância; ativação depende de aprovação do master.
- E-mail: uma única conta SMTP da plataforma, configurada pelo master.
- **Sem** moradores/unidades, reservas de área comum, prestação de contas/financeiro do condomínio, ocorrências, fornecedores ou votações — sistema é um cadastro de referência, não um ERP de condomínio.
- Contatos (síndico/subsíndico/conselheiro) são entidades desacopladas dos condomínios (N:N via `contato_condominio`) — a mesma pessoa pode atender mais de um condomínio.
- Funcionários do escritório podem ser vinculados a condomínios específicos e também recebem avisos/alertas.
- Assembleias são uma entidade própria; avisos podem opcionalmente referenciar uma assembleia e ter documentos anexados.
## Notas / pendências conhecidas (não bloqueiam o MVP, mas ficam registradas)
- Provisionamento de tenant (Prompt 05) roda de forma síncrona na criação da empresa — migrar para fila assíncrona é uma melhoria futura.
- Envio de aviso (Prompt 08) roda de forma síncrona — idem, fila assíncrona é melhoria futura.
- Cache de conexão por tenant (Prompt 03) não tem invalidação automática quando uma empresa é bloqueada/cancelada — só um método pronto, não chamado ainda.
- Funcionalidades "Fase 2" do catálogo (templates, avisos recorrentes, webhooks, busca avançada) estão seedadas no banco mas sem handler implementado.