7.9 KiB
7.9 KiB
1. Schema: tabela categoria e coluna categoria_id
- 1.1 Adicionar
categoria(id INTEGER PRIMARY KEY AUTOINCREMENT,categoria TEXT NOT NULL,palavra_chave TEXT NOT NULL) aoSCHEMA/executescriptemapp/database.py(CREATE TABLE IF NOT EXISTS) - 1.2 Adicionar coluna
categoria_id INTEGER REFERENCES categoria(id)(nullable) emfiscal_documents, com checagem viaPRAGMA table_info(fiscal_documents)antes doALTER TABLEpara não falhar em bancos já existentes - 1.3 Adicionar insert idempotente
INSERT OR IGNORE INTO categoria (id, categoria, palavra_chave) VALUES (1, 'Não Encontrado', '')no mesmo passo de inicialização do schema, garantindo que a linha reservadaid = 1sempre exista sem nunca sobrescrever edições feitas pelo usuário - 1.4 Adicionar helpers em
app/database.py:create_categoria(conn, categoria, palavra_chave),get_categoria(conn, categoria_id),list_categorias(conn),update_categoria(conn, categoria_id, categoria, palavra_chave),delete_categoria(conn, categoria_id)(deve rejeitarcategoria_id == 1sem alterar nada; caso contrário, fazerUPDATE fiscal_documents SET categoria_id = 1 WHERE categoria_id = ?antes de excluir a linha, na mesma transação),find_categoria_by_keyword(conn, supplier_name)(busca case-insensitive depalavra_chavecomo substring desupplier_name, excluindoid = 1, ordenado porid ASC, retorna a primeira correspondência — importante:palavra_chavevazia da linha reservada é substring de qualquer texto, então incluí-la quebraria o casamento de palavras-chave reais)
2. Módulo de categorização com limites de segurança
- 2.1 Criar
app/categorization.pycom funçãocategorize_supplier(supplier_name: str, conn, batch_id) -> int(sempre retorna umcategoria.idválido, nuncaNone) implementando a ordem: cache em memória → limite por lote → IA → palavra-chave →1("Não Encontrado") - 2.2 Implementar cache em memória por fornecedor normalizado (
strip().upper()), populado após qualquer categorização (IA, palavra-chave, ou1quando nada é encontrado), consultado antes de qualquer chamada de IA - 2.3 Implementar contador de chamadas de IA por
import_batches.id, comparado asettings.CATEGORIZATION_MAX_AI_CALLS_PER_BATCH; ao atingir o limite, pular direto para o fallback de palavra-chave (e depois1se nada corresponder) - 2.4 Implementar chamada de IA dedicada (OpenAI
chat.completions.create, prompt curto comsupplier_name+ lista de categorias já existentes na tabelacategoria), validando que a resposta pertence ao conjunto conhecido; qualquer exceção ou valor fora do conjunto é tratado como "sem categoria da IA" (sem retry) - 2.5 Adicionar
CATEGORIZATION_MAX_AI_CALLS_PER_BATCH(default configurável) emapp/config.py - 2.6 Adicionar logs (INFO/DEBUG) quando uma chamada de IA é pulada por cache hit ou por limite de lote atingido, incluindo fornecedor e motivo
3. Integração no fluxo de ingestão
- 3.1 Chamar
categorize_supplierno momento em que ofiscal_documentsé criado/confirmado, persistindo ocategoria_idretornado (sempre um valor válido, no mínimo1) — implementado emdatabase.py::confirm_batch(não emingestion.py: a criação/confirmação defiscal_documentsacontece emdatabase.py,ingestion.pysó extrai) - 3.2 Garantir que falha na categorização (qualquer exceção não tratada dentro do módulo) não impede a criação do
fiscal_documents— a nota deve ser salva comcategoria_id = 1("Não Encontrado") nesse caso
4. Dashboard: exibição e filtro por categoria
- 4.1 Criar função
category_totals(conn, start, end)emapp/database.py(mesmo padrão demonthly_totals/supplier_totals), agrupando porcategoria.categoria(via LEFT JOIN, incluindo a linhaid = 1"Não Encontrado") e um grupo separado "Sem categoria" apenas paracategoria_id IS NULL(documentos legados não reprocessados) - 4.2 Adicionar parâmetro opcional
categoryemdashboard_routes.py, propagando o filtro paramonthly_totals,supplier_totals,category_totalse demais consultas da página - 4.3 Atualizar
app/templates/dashboard.htmlcom um gráfico/lista de gastos por categoria e um controle de filtro (select) populado a partir delist_categorias(inclui "Não Encontrado") + opção "Sem categoria" para os registros legados - 4.4 Validado (sem extensão de navegador disponível na sessão: verificado via requisições HTTP autenticadas + inspeção do HTML renderizado, e via chamadas diretas às funções de banco simulando um lote confirmado) — Dashboard exibe totais por categoria e o filtro reduz corretamente KPIs/gráficos/lista de documentos recentes
5. CRUD administrativo da tabela categoria
- 5.1 Criar
app/routes/categoria_routes.pycomrouter = APIRouter()e as rotasGET /categorias,GET/POST /categorias/new,GET/POST /categorias/{categoria_id}/edit,POST /categorias/{categoria_id}/delete, seguindo exatamente o padrão dedocuments_routes.py(uso derender(),flash(),auth.check_csrf) - 5.2 Registrar
categoria_routesemapp/main.py(import +app.include_router(categoria_routes.router)), no mesmo bloco onde os demais routers são registrados - 5.3 Criar
app/templates/categorias_list.html({% extends "base.html" %}) reaproveitando o layout dedocuments_list.html:.page-headcom botão "+ Nova", tabela (card/table/table-scroll) listandoid,categoria,palavra_chave, e ações de editar/excluir por linha (form inline comonsubmit="return confirm(...)"ecsrf_tokenoculto, igual ao padrão de exclusão de documentos); a linhaid = 1("Não Encontrado") mostra a ação de excluir desabilitada/oculta, já que a exclusão é sempre rejeitada - 5.4 Criar
app/templates/categoria_form.html({% extends "base.html" %}) reaproveitando o layout dedocument_form.html, com variávelmode("new"/"edit") controlando aactiondo form entre/categorias/newe/categorias/{id}/edit - 5.5 Adicionar link "Categorias" na
<nav class="nav">deapp/templates/base.html, com a mesma lógica de classeactive(request.url.path.startswith('/categorias')) usada pelos demais links - 5.6 Validado (sem extensão de navegador disponível na sessão: driver via requisições HTTP autenticadas contra o servidor real, cookies de sessão + CSRF) o fluxo completo: criar, listar, editar e excluir uma categoria; confirmado que excluir a categoria
id = 1é rejeitado, e que excluir uma categoria em uso reatribui os documentos relacionados para "Não Encontrado" sem erro
6. Testes e validação
- 6.1 Testes unitários para
find_categoria_by_keyword(match, no match, case-insensitive, múltiplos candidatos → primeiro porid, e confirmando queid = 1nunca é retornado por essa função mesmo compalavra_chavevazia) - 6.2 Testes unitários para
categorize_suppliercobrindo: cache hit (sem chamada de IA), limite de lote atingido (sem chamada de IA), IA retorna categoria válida, IA falha/retorna inválido → fallback por palavra-chave, nenhum método encontra categoria → retorna1 - 6.3 Teste de integração do fluxo de ingestão ponta a ponta: nota processada resulta em
fiscal_documents.categoria_idcorreto nos cenários de IA, fallback por palavra-chave e "Não Encontrado" (1) - 6.4 Testes unitários/integração para o CRUD de
categoria: criar, ler, atualizar, excluir (incluindo excluir categoria referenciada porfiscal_documents→ reatribuição para1, e tentar excluirid = 1→ rejeitado) - 6.5 Automatizado como equivalente ao teste manual (
test_repeated_suppliers_call_ai_once_each_and_respect_batch_limitemtests/test_categorization.py, usando mock +assertLogs): lote com fornecedores repetidos confirma que a IA é chamada apenas uma vez por fornecedor distinto e que o limite por lote é respeitado