plan: add implementation plan for fraud detection engine

Stack: Python 3.12/FastAPI + React 19/TypeScript + PostgreSQL + Redis.
6 artifacts: plan.md, research.md, data-model.md, api.yaml (OpenAPI),
quickstart.md, updated AGENTS.md. Constitution check: 6/6 PASS.
This commit is contained in:
Felipe Domingues 2026-05-12 14:58:21 -03:00
parent e31cba2847
commit 7f9832bf4b
6 changed files with 1487 additions and 1 deletions

View file

@ -0,0 +1,175 @@
# Data Model: Motor de Detecção de Fraudes Bancárias
**Feature**: 001-fraud-detection-engine
**Date**: 2026-05-11
## Entity Relationship Diagram
```
┌─────────────┐ ┌──────────────────┐ ┌─────────────┐
│ Transaction │──────▶│ FraudAlert │◀──────│ DetectionRule│
└─────────────┘ 1:N └──────────────────┘ N:M └─────────────┘
│ 1:1
┌──────────────────┐
│InvestigationResult│
└──────────────────┘
┌──────────────────┐
│ AuditLog │ (todos os eventos)
└──────────────────┘
```
## Entities
### Transaction
| Field | Type | Constraints | Description |
|-------|------|-------------|-------------|
| `id` | UUID | PK, NOT NULL | Identificador único da transação |
| `external_id` | VARCHAR(255) | NOT NULL, UNIQUE | ID original do sistema bancário |
| `amount` | DECIMAL(15,2) | NOT NULL, CHECK > 0 | Valor da transação em reais |
| `merchant_name` | VARCHAR(255) | NOT NULL | Nome do estabelecimento |
| `merchant_category` | VARCHAR(100) | NOT NULL | Categoria (ex: "alimentação", "eletrônicos") |
| `latitude` | DECIMAL(10,7) | NULLABLE | Latitude do estabelecimento |
| `longitude` | DECIMAL(10,7) | NULLABLE | Longitude do estabelecimento |
| `customer_id` | VARCHAR(128) | NOT NULL, INDEX | ID do cliente tokenizado |
| `channel` | VARCHAR(20) | NOT NULL, CHECK (web, mobile, pos) | Canal da transação |
| `transaction_at` | TIMESTAMPTZ | NOT NULL, INDEX | Timestamp original da transação |
| `ingested_at` | TIMESTAMPTZ | NOT NULL, DEFAULT NOW() | Timestamp de ingestão no FraudShield |
| `ruleset_version` | VARCHAR(20) | NOT NULL | Versão do conjunto de regras usado |
| `raw_payload` | JSONB | NOT NULL | Payload original completo para auditoria |
**Indexes**:
- `idx_transactions_customer_id` on `customer_id`
- `idx_transactions_transaction_at` on `transaction_at`
- `idx_transactions_ingested_at` on `ingested_at`
### DetectionRule
| Field | Type | Constraints | Description |
|-------|------|-------------|-------------|
| `id` | UUID | PK, NOT NULL | Identificador único da regra |
| `name` | VARCHAR(255) | NOT NULL, UNIQUE | Nome da regra |
| `description` | TEXT | NOT NULL | Descrição do que a regra detecta |
| `condition` | TEXT | NOT NULL | Expressão da condição (ex: `amount > customer.avg_amount * 3`) |
| `weight` | INTEGER | NOT NULL, CHECK 0-100, DEFAULT 0 | Peso da regra no score composto |
| `threshold` | INTEGER | NOT NULL, CHECK 0-100, DEFAULT 0 | Score mínimo individual para disparar |
| `is_active` | BOOLEAN | NOT NULL, DEFAULT FALSE | Se a regra está ativa em produção |
| `version` | INTEGER | NOT NULL, DEFAULT 1 | Versão incremental |
| `created_by` | VARCHAR(255) | NOT NULL | Usuário que criou |
| `created_at` | TIMESTAMPTZ | NOT NULL, DEFAULT NOW() | Data de criação |
| `updated_at` | TIMESTAMPTZ | NOT NULL, DEFAULT NOW() | Data da última modificação |
### FraudAlert
| Field | Type | Constraints | Description |
|-------|------|-------------|-------------|
| `id` | UUID | PK, NOT NULL | Identificador único do alerta |
| `transaction_id` | UUID | FK → Transaction.id, NOT NULL | Transação associada |
| `score` | INTEGER | NOT NULL, CHECK 0-100 | Score de risco composto |
| `triggered_rules` | JSONB | NOT NULL | Array de objetos: `[{rule_id, rule_name, score, explanation}]` |
| `explanation` | TEXT | NOT NULL | Explicação composta em linguagem natural |
| `status` | VARCHAR(20) | NOT NULL, DEFAULT 'pending', CHECK (pending, confirmed, false_positive, escalated) | Status do alerta |
| `assigned_to` | VARCHAR(255) | NULLABLE | Analista designado |
| `decision` | VARCHAR(20) | NULLABLE, CHECK (confirmed, false_positive, escalated) | Decisão tomada |
| `decided_by` | VARCHAR(255) | NULLABLE | Analista que decidiu |
| `decided_at` | TIMESTAMPTZ | NULLABLE | Timestamp da decisão |
| `justification` | TEXT | NULLABLE | Justificativa (obrigatória para false_positive e escalated) |
| `created_at` | TIMESTAMPTZ | NOT NULL, DEFAULT NOW() | Timestamp de criação do alerta |
| `ruleset_version` | VARCHAR(20) | NOT NULL | Versão do ruleset que gerou o alerta |
**Indexes**:
- `idx_alerts_status` on `status`
- `idx_alerts_created_at` on `created_at`
- `idx_alerts_transaction_id` on `transaction_id`
### AuditLog
| Field | Type | Constraints | Description |
|-------|------|-------------|-------------|
| `id` | UUID | PK, NOT NULL | Identificador único |
| `trace_id` | UUID | NOT NULL, INDEX | Trace ID para correlação |
| `event_type` | VARCHAR(50) | NOT NULL, INDEX | Tipo de evento (transaction_received, rule_evaluated, alert_created, decision_made, rule_updated) |
| `event_at` | TIMESTAMPTZ | NOT NULL, INDEX | Timestamp do evento |
| `actor` | VARCHAR(255) | NULLABLE | Usuário ou sistema que gerou o evento |
| `payload` | JSONB | NOT NULL | Dados completos do evento |
| `ruleset_version` | VARCHAR(20) | NULLABLE | Versão do ruleset no momento do evento |
**Partitioning**: Por mês via `event_at` para facilitar retenção e performance.
**Imutabilidade**: Tabela configurada com triggers que rejeitam UPDATE e DELETE. Apenas INSERT permitido.
### RuleSnapshot
| Field | Type | Constraints | Description |
|-------|------|-------------|-------------|
| `version` | VARCHAR(20) | PK, NOT NULL | Versão do conjunto de regras (ex: "20260511-001") |
| `rules_json` | JSONB | NOT NULL | Snapshot completo de todas as regras ativas na versão |
| `created_at` | TIMESTAMPTZ | NOT NULL, DEFAULT NOW() | Quando o snapshot foi criado |
| `created_by` | VARCHAR(255) | NOT NULL | Quem gerou o snapshot |
### CustomerProfile (Cache / Materialized View)
| Field | Type | Source | Description |
|-------|------|--------|-------------|
| `customer_id` | VARCHAR(128) | Derived | ID do cliente tokenizado |
| `avg_amount_30d` | DECIMAL(15,2) | AVG(amount) 30d | Média de gasto 30 dias |
| `avg_amount_90d` | DECIMAL(15,2) | AVG(amount) 90d | Média de gasto 90 dias |
| `common_categories` | JSONB | Top 5 categorias 90d | Categorias mais frequentes |
| `common_locations` | JSONB | Cluster de coordenadas | Regiões habituais de transação |
| `night_transactions_pct` | DECIMAL(5,2) | % transações 22h-06h | Percentual noturno |
| `last_updated_at` | TIMESTAMPTZ | NOW() | Última atualização |
**Implementation**: Materialized view no PostgreSQL, refresh a cada 5 minutos. Cache em Redis com TTL de 5 minutos para acesso rápido pelo motor de regras.
## State Transitions
### Alert Lifecycle
```
┌──────────────┐
│ pending │ ◀── Alerta criado (score > threshold)
└──────┬───────┘
┌────────────┼────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌───────────┐
│confirmed │ │ false_ │ │ escalated │
│ │ │ positive │ │ │
└──────────┘ └──────────┘ └─────┬─────┘
Revisão sênior
┌────────┼────────┐
▼ ▼
┌──────────┐ ┌──────────┐
│confirmed │ │ false_ │
│ │ │ positive │
└──────────┘ └──────────┘
Estados finais: confirmed, false_positive (não transicionam mais)
```
### Rule Lifecycle
```
┌────────┐ activate ┌────────┐
│inactive│───────────────▶│ active │
└────────┘ └────────┘
▲ │
│ deactivate │
└──────────────────────────┘
```
## Validation Rules
- `Transaction.amount` > 0
- `Transaction.channel` ∈ {web, mobile, pos}
- `DetectionRule.weight` ∈ [0, 100]
- `DetectionRule.threshold` ∈ [0, 100]
- `FraudAlert.score` ∈ [0, 100]
- `FraudAlert.justification` required when decision ∈ {false_positive, escalated}
- `FraudAlert.status` transitions only: pending → {confirmed, false_positive, escalated}; escalated → {confirmed, false_positive}