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,803 @@
openapi: "3.0.3"
info:
title: FraudShield API
version: "1.0.0"
description: API do motor de detecção de fraudes bancárias.
servers:
- url: http://localhost:8000/api/v1
description: Local development
paths:
/transactions/ingest:
post:
summary: Ingest transaction for fraud evaluation
operationId: ingestTransaction
tags: [Transactions]
security:
- ApiKeyAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/TransactionIngestRequest"
responses:
"202":
description: Transaction accepted for processing
content:
application/json:
schema:
$ref: "#/components/schemas/TransactionAccepted"
"400":
$ref: "#/components/responses/ValidationError"
"429":
description: Rate limit exceeded
"503":
description: Service unavailable (backpressure)
/transactions/{transaction_id}:
get:
summary: Get transaction details with evaluation result
operationId: getTransaction
tags: [Transactions]
security:
- BearerAuth: []
parameters:
- name: transaction_id
in: path
required: true
schema:
type: string
format: uuid
responses:
"200":
description: Transaction details
content:
application/json:
schema:
$ref: "#/components/schemas/TransactionDetail"
"404":
description: Transaction not found
/alerts:
get:
summary: List fraud alerts with filters
operationId: listAlerts
tags: [Alerts]
security:
- BearerAuth: []
parameters:
- name: status
in: query
schema:
type: string
enum: [pending, confirmed, false_positive, escalated]
- name: min_score
in: query
schema:
type: integer
minimum: 0
maximum: 100
- name: from_date
in: query
schema:
type: string
format: date-time
- name: to_date
in: query
schema:
type: string
format: date-time
- name: triggered_rule
in: query
schema:
type: string
- name: limit
in: query
schema:
type: integer
default: 50
maximum: 200
- name: offset
in: query
schema:
type: integer
default: 0
responses:
"200":
description: Paginated alert list
content:
application/json:
schema:
$ref: "#/components/schemas/AlertListResponse"
/alerts/{alert_id}:
get:
summary: Get alert details
operationId: getAlert
tags: [Alerts]
security:
- BearerAuth: []
parameters:
- name: alert_id
in: path
required: true
schema:
type: string
format: uuid
responses:
"200":
description: Alert details with full explanation
content:
application/json:
schema:
$ref: "#/components/schemas/AlertDetail"
"404":
description: Alert not found
/alerts/{alert_id}/decide:
post:
summary: Submit investigation decision
operationId: decideAlert
tags: [Alerts]
security:
- BearerAuth: []
parameters:
- name: alert_id
in: path
required: true
schema:
type: string
format: uuid
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/AlertDecisionRequest"
responses:
"200":
description: Decision recorded
content:
application/json:
schema:
$ref: "#/components/schemas/AlertDetail"
"400":
$ref: "#/components/responses/ValidationError"
"409":
description: Alert already decided
/rules:
get:
summary: List detection rules
operationId: listRules
tags: [Rules]
security:
- BearerAuth: []
- AdminAuth: []
parameters:
- name: is_active
in: query
schema:
type: boolean
responses:
"200":
description: Rule list
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/DetectionRule"
post:
summary: Create new detection rule
operationId: createRule
tags: [Rules]
security:
- BearerAuth: []
- AdminAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateRuleRequest"
responses:
"201":
description: Rule created (inactive by default)
content:
application/json:
schema:
$ref: "#/components/schemas/DetectionRule"
"400":
$ref: "#/components/responses/ValidationError"
/rules/{rule_id}:
get:
summary: Get rule details
operationId: getRule
tags: [Rules]
security:
- BearerAuth: []
- AdminAuth: []
parameters:
- name: rule_id
in: path
required: true
schema:
type: string
format: uuid
responses:
"200":
description: Rule details
content:
application/json:
schema:
$ref: "#/components/schemas/DetectionRule"
put:
summary: Update detection rule
operationId: updateRule
tags: [Rules]
security:
- BearerAuth: []
- AdminAuth: []
parameters:
- name: rule_id
in: path
required: true
schema:
type: string
format: uuid
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateRuleRequest"
responses:
"200":
description: Rule updated
content:
application/json:
schema:
$ref: "#/components/schemas/DetectionRule"
/rules/{rule_id}/activate:
post:
summary: Activate a rule
operationId: activateRule
tags: [Rules]
security:
- BearerAuth: []
- AdminAuth: []
parameters:
- name: rule_id
in: path
required: true
schema:
type: string
format: uuid
responses:
"200":
description: Rule activated
"400":
description: Rule has no weight or threshold set
/rules/{rule_id}/deactivate:
post:
summary: Deactivate a rule
operationId: deactivateRule
tags: [Rules]
security:
- BearerAuth: []
- AdminAuth: []
parameters:
- name: rule_id
in: path
required: true
schema:
type: string
format: uuid
responses:
"200":
description: Rule deactivated
/rules/{rule_id}/test:
post:
summary: Test rule against historical data
operationId: testRule
tags: [Rules]
security:
- BearerAuth: []
- AdminAuth: []
parameters:
- name: rule_id
in: path
required: true
schema:
type: string
format: uuid
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/TestRuleRequest"
responses:
"200":
description: Test results
content:
application/json:
schema:
$ref: "#/components/schemas/TestRuleResponse"
/dashboard/metrics:
get:
summary: Get real-time dashboard metrics
operationId: getDashboardMetrics
tags: [Dashboard]
security:
- BearerAuth: []
parameters:
- name: period
in: query
schema:
type: string
enum: [24h, 7d, 30d, 90d]
default: 24h
responses:
"200":
description: Dashboard metrics
content:
application/json:
schema:
$ref: "#/components/schemas/DashboardMetrics"
/dashboard/report:
get:
summary: Export compliance report
operationId: exportReport
tags: [Dashboard]
security:
- BearerAuth: []
parameters:
- name: from_date
in: query
required: true
schema:
type: string
format: date
- name: to_date
in: query
required: true
schema:
type: string
format: date
- name: format
in: query
schema:
type: string
enum: [pdf, csv]
default: pdf
responses:
"200":
description: Report file
content:
application/pdf:
schema:
type: string
format: binary
text/csv:
schema:
type: string
format: binary
/health:
get:
summary: Health check
operationId: healthCheck
tags: [System]
responses:
"200":
description: Service healthy
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: healthy
version:
type: string
example: "1.0.0"
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
AdminAuth: {}
schemas:
TransactionIngestRequest:
type: object
required: [external_id, amount, merchant_name, merchant_category, customer_id, channel, transaction_at]
properties:
external_id:
type: string
description: ID original do sistema bancário
amount:
type: number
format: decimal
minimum: 0.01
example: 5000.00
merchant_name:
type: string
example: "Loja Exemplo Ltda"
merchant_category:
type: string
example: "eletrônicos"
latitude:
type: number
format: float
nullable: true
longitude:
type: number
format: float
nullable: true
customer_id:
type: string
description: Token do cliente
example: "tok_abc123xyz"
channel:
type: string
enum: [web, mobile, pos]
transaction_at:
type: string
format: date-time
TransactionAccepted:
type: object
properties:
transaction_id:
type: string
format: uuid
status:
type: string
example: accepted
trace_id:
type: string
format: uuid
TransactionDetail:
type: object
properties:
id:
type: string
format: uuid
external_id:
type: string
amount:
type: number
merchant_name:
type: string
merchant_category:
type: string
latitude:
type: number
nullable: true
longitude:
type: number
nullable: true
customer_id:
type: string
channel:
type: string
transaction_at:
type: string
format: date-time
ingested_at:
type: string
format: date-time
alert:
$ref: "#/components/schemas/AlertDetail"
nullable: true
AlertListResponse:
type: object
properties:
items:
type: array
items:
$ref: "#/components/schemas/AlertSummary"
total:
type: integer
limit:
type: integer
offset:
type: integer
AlertSummary:
type: object
properties:
id:
type: string
format: uuid
score:
type: integer
status:
type: string
amount:
type: number
merchant_name:
type: string
top_rule:
type: string
created_at:
type: string
format: date-time
AlertDetail:
type: object
properties:
id:
type: string
format: uuid
transaction_id:
type: string
format: uuid
score:
type: integer
triggered_rules:
type: array
items:
type: object
properties:
rule_id:
type: string
format: uuid
rule_name:
type: string
score:
type: integer
explanation:
type: string
explanation:
type: string
description: Explicação composta em linguagem natural
status:
type: string
enum: [pending, confirmed, false_positive, escalated]
assigned_to:
type: string
nullable: true
decision:
type: string
nullable: true
decided_by:
type: string
nullable: true
decided_at:
type: string
format: date-time
nullable: true
justification:
type: string
nullable: true
created_at:
type: string
format: date-time
AlertDecisionRequest:
type: object
required: [decision]
properties:
decision:
type: string
enum: [confirmed, false_positive, escalated]
justification:
type: string
description: Required for false_positive and escalated
notes:
type: string
DetectionRule:
type: object
properties:
id:
type: string
format: uuid
name:
type: string
description:
type: string
condition:
type: string
weight:
type: integer
minimum: 0
maximum: 100
threshold:
type: integer
minimum: 0
maximum: 100
is_active:
type: boolean
version:
type: integer
created_by:
type: string
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
CreateRuleRequest:
type: object
required: [name, description, condition]
properties:
name:
type: string
example: "Valor atípico"
description:
type: string
example: "Detecta transações com valor muito acima da média do cliente"
condition:
type: string
example: "amount > customer.avg_amount_30d * 3"
weight:
type: integer
minimum: 0
maximum: 100
default: 0
threshold:
type: integer
minimum: 0
maximum: 100
default: 0
UpdateRuleRequest:
type: object
properties:
name:
type: string
description:
type: string
condition:
type: string
weight:
type: integer
minimum: 0
maximum: 100
threshold:
type: integer
minimum: 0
maximum: 100
TestRuleRequest:
type: object
properties:
days_back:
type: integer
default: 90
maximum: 90
TestRuleResponse:
type: object
properties:
rule_id:
type: string
format: uuid
total_transactions_evaluated:
type: integer
example: 50000
would_trigger_count:
type: integer
example: 1250
would_trigger_pct:
type: number
example: 2.5
score_distribution:
type: object
properties:
p50:
type: number
p90:
type: number
p95:
type: number
max:
type: number
estimated_false_positives:
type: integer
description: Based on analyst decisions on similar patterns
sample_alerts:
type: array
items:
$ref: "#/components/schemas/AlertSummary"
DashboardMetrics:
type: object
properties:
period:
type: string
total_transactions:
type: integer
total_alerts:
type: integer
fraud_rate_pct:
type: number
description: Alertas confirmados / total de transações
false_positive_rate_pct:
type: number
avg_decision_time_seconds:
type: number
alerts_by_status:
type: object
properties:
pending:
type: integer
confirmed:
type: integer
false_positive:
type: integer
escalated:
type: integer
alerts_by_hour:
type: array
items:
type: object
properties:
hour:
type: integer
count:
type: integer
top_triggering_rules:
type: array
items:
type: object
properties:
rule_name:
type: string
alert_count:
type: integer
score_distribution:
type: object
properties:
low:
type: integer
medium:
type: integer
high:
type: integer
critical:
type: integer
responses:
ValidationError:
description: Validation error
content:
application/json:
schema:
type: object
properties:
detail:
type: string
errors:
type: array
items:
type: object
properties:
field:
type: string
message:
type: string

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}

View file

@ -0,0 +1,177 @@
# Implementation Plan: Motor de Detecção de Fraudes Bancárias
**Branch**: `001-fraud-detection-engine` | **Date**: 2026-05-11 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `/specs/001-fraud-detection-engine/spec.md`
## Summary
Sistema web para detecção de fraudes bancárias com motor de regras pontuável, explicações auditáveis em linguagem natural, fila de investigação para analistas, gestão de regras por administradores, e dashboard de métricas em tempo real. Backend Python/FastAPI com pipeline de ingestão assíncrono via Redis Streams, frontend React/TypeScript com dashboard interativo, PostgreSQL para dados transacionais e audit log imutável.
## Technical Context
**Language/Version**: Python 3.12 (backend), TypeScript 5.x (frontend)
**Primary Dependencies**: FastAPI, SQLAlchemy 2.0, Pydantic v2, Alembic, React 19, Tailwind CSS, Recharts
**Storage**: PostgreSQL 16 (transacional + audit log), Redis 7 (cache + filas + real-time)
**Testing**: pytest + pytest-asyncio (backend), Vitest + React Testing Library (frontend), Playwright (E2E)
**Target Platform**: Web browser (frontend) + REST API (backend), Docker Compose (dev/local)
**Project Type**: Web service (backend API + SPA frontend)
**Performance Goals**: 500 transações/minuto com p95 < 2s de latência de processamento
**Constraints**: p95 < 2s (processamento), p95 < 60s (atraso dashboard), audit log imutável, LGPD compliance
**Scale/Scope**: 10.000 transações/dia inicial, picos de 5x, 3 perfis de usuário, ~5-20 regras ativas
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
| Principle | Status | Notes |
|-----------|--------|-------|
| I. Security-First | ✅ PASS | JWT + OAuth2 via IdP externo, API keys para ingestão, TLS 1.3, secrets via env vars, PII tokenizado antes da ingestão |
| II. Explainability | ✅ PASS | Cada regra gera explicação em linguagem natural. Explicação composta agregada no alerta. Formato JSON + texto |
| III. Test-First | ✅ PASS | TDD planejado: testes de regra, contrato, integração, E2E. Métricas de FP/FN como gate de deploy |
| IV. Observability | ✅ PASS | Audit log imutável com trace ID. Structured logging (JSON). Métricas exportáveis para dashboard |
| V. Data Privacy | ✅ PASS | Dados chegam tokenizados. Sem PII armazenado. LGPD: direito à explicação, exclusão, DPIA. Mascaramento em logs/UI |
| VI. Simplicity (YAGNI) | ✅ PASS | Rules-based v1 (sem ML). Monolith FastAPI (sem microservices). PostgreSQL sem TimescaleDB até necessidade comprovada. Redis Streams em vez de Kafka |
**Gate Result**: ALL PASS — proceeding to implementation.
## Project Structure
### Documentation (this feature)
```text
specs/001-fraud-detection-engine/
├── plan.md # This file
├── research.md # Technology research & decisions
├── data-model.md # Entity definitions, relationships, state machines
├── quickstart.md # Setup, test credentials, smoke test flow
├── contracts/
│ └── api.yaml # OpenAPI 3.0 specification
└── tasks.md # Phase 2 output (NOT created by /speckit.plan)
```
### Source Code (repository root)
```text
backend/
├── fraudshield/
│ ├── __init__.py
│ ├── main.py # FastAPI application factory
│ ├── config.py # pydantic-settings configuration
│ ├── auth.py # JWT validation, OAuth2 middleware, role checks
│ ├── db.py # SQLAlchemy engine, session, base
│ ├── models/
│ │ ├── __init__.py
│ │ ├── transaction.py # Transaction ORM model
│ │ ├── rule.py # DetectionRule ORM model
│ │ ├── alert.py # FraudAlert ORM model
│ │ └── audit.py # AuditLog ORM model (immutable)
│ ├── schemas/
│ │ ├── __init__.py
│ │ ├── transaction.py # Pydantic request/response schemas
│ │ ├── rule.py
│ │ ├── alert.py
│ │ └── dashboard.py
│ ├── engine/
│ │ ├── __init__.py
│ │ ├── evaluator.py # Core: iterate rules, compute composite score
│ │ ├── explainer.py # Generate natural language explanations
│ │ ├── loader.py # Load active rules from DB, cache in memory
│ │ └── rules/ # Individual rule implementations
│ │ ├── __init__.py
│ │ ├── base.py # Abstract Rule base class
│ │ ├── amount.py # Amount anomaly rules
│ │ ├── location.py # Geolocation rules
│ │ ├── time.py # Time-based rules
│ │ └── pattern.py # Customer pattern rules
│ ├── api/
│ │ ├── __init__.py
│ │ ├── deps.py # Dependency injection (DB session, current user)
│ │ ├── transactions.py # POST /transactions/ingest, GET /transactions/{id}
│ │ ├── alerts.py # GET /alerts, GET /alerts/{id}, POST /alerts/{id}/decide
│ │ ├── rules.py # CRUD + activate/deactivate + test
│ │ ├── dashboard.py # GET /dashboard/metrics, GET /dashboard/report
│ │ └── health.py # GET /health
│ ├── services/
│ │ ├── __init__.py
│ │ ├── ingestion.py # Transaction ingestion + queue dispatch
│ │ ├── detection.py # Orchestrate rule evaluation pipeline
│ │ ├── alerts.py # Alert creation, assignment, decision
│ │ ├── rules.py # Rule CRUD, activation, snapshot
│ │ └── dashboard.py # Metrics aggregation from DB + Redis
│ └── seed.py # Development data seeder
├── alembic/
│ ├── env.py
│ └── versions/
├── tests/
│ ├── conftest.py # Fixtures: test DB, test client, auth tokens
│ ├── unit/
│ │ ├── test_evaluator.py
│ │ ├── test_explainer.py
│ │ └── test_rules/
│ │ ├── test_amount.py
│ │ ├── test_location.py
│ │ └── test_time.py
│ ├── integration/
│ │ ├── test_ingestion_pipeline.py
│ │ ├── test_alert_workflow.py
│ │ └── test_rule_lifecycle.py
│ └── contract/
│ ├── test_transactions_api.py
│ ├── test_alerts_api.py
│ └── test_rules_api.py
├── pyproject.toml
└── Dockerfile
frontend/
├── src/
│ ├── api/
│ │ └── client.ts # Generated OpenAPI client
│ ├── components/
│ │ ├── layout/
│ │ │ ├── Shell.tsx
│ │ │ └── Navbar.tsx
│ │ ├── alerts/
│ │ │ ├── AlertQueue.tsx
│ │ │ ├── AlertCard.tsx
│ │ │ └── AlertDetail.tsx
│ │ ├── rules/
│ │ │ ├── RuleList.tsx
│ │ │ ├── RuleForm.tsx
│ │ │ └── RuleTestResults.tsx
│ │ └── dashboard/
│ │ ├── MetricCard.tsx
│ │ ├── AlertsByHour.tsx
│ │ ├── TopRules.tsx
│ │ └── ScoreDistribution.tsx
│ ├── pages/
│ │ ├── LoginPage.tsx
│ │ ├── AlertsPage.tsx
│ │ ├── RulesPage.tsx
│ │ └── DashboardPage.tsx
│ ├── hooks/
│ │ ├── useAuth.ts
│ │ ├── useAlerts.ts
│ │ └── useDashboard.ts
│ ├── contexts/
│ │ └── AuthContext.tsx
│ ├── lib/
│ │ └── utils.ts
│ ├── App.tsx
│ └── main.tsx
├── tests/
│ ├── components/
│ └── pages/
├── package.json
├── tsconfig.json
├── tailwind.config.ts
└── vite.config.ts
docker-compose.yml
AGENTS.md
```
**Structure Decision**: Web application with separate `backend/` and `frontend/` directories. Backend is a single FastAPI service (monolith per YAGNI principle). Frontend is a Vite + React SPA.
## Complexity Tracking
> No constitutional violations to justify. All gates passed.

View file

@ -0,0 +1,190 @@
# Quickstart: FraudShield
**Feature**: 001-fraud-detection-engine
## Pré-requisitos
- Docker & Docker Compose
- Python 3.12+
- Node.js 20+
- `uv` (Python package manager)
## Setup Local (Dev)
```bash
# 1. Clone e entre no projeto
git clone <repo-url> fraudshield
cd fraudshield
# 2. Suba os serviços de infra
docker compose up -d postgres redis
# 3. Backend
cd backend
uv sync # Instala dependências
uv run alembic upgrade head # Cria tabelas
uv run python -m fraudshield.seed # Popula regras iniciais + dados de teste
uv run uvicorn fraudshield.main:app --reload --port 8000
# 4. Frontend (outro terminal)
cd frontend
npm install
npm run dev # http://localhost:5173
# 5. Testes
cd backend && uv run pytest # Backend
cd frontend && npm test # Frontend
```
## Infraestrutura Docker
```yaml
# docker-compose.yml (visão geral)
services:
postgres:
image: postgres:16
environment:
POSTGRES_DB: fraudshield
POSTGRES_USER: fraudshield
POSTGRES_PASSWORD: fraudshield_dev
ports: ["5432:5432"]
volumes:
- pgdata:/var/lib/postgresql/data
redis:
image: redis:7-alpine
ports: ["6379:6379"]
```
## Credenciais de Teste
| Perfil | Usuário | Senha |
|--------|---------|-------|
| Analista | `analista@fraudshield.local` | `fraudshield123` |
| Administrador | `admin@fraudshield.local` | `fraudshield123` |
| Sênior | `senior@fraudshield.local` | `fraudshield123` |
## Fluxo de Teste Manual
### 1. Ingerir uma transação suspeita
```bash
curl -X POST http://localhost:8000/api/v1/transactions/ingest \
-H "Content-Type: application/json" \
-H "X-API-Key: dev-api-key" \
-d '{
"external_id": "txn_test_001",
"amount": 15000.00,
"merchant_name": "Loja Desconhecida Ltda",
"merchant_category": "joalheria",
"latitude": -23.5505,
"longitude": -46.6333,
"customer_id": "tok_cust_001",
"channel": "web",
"transaction_at": "2026-05-11T03:15:00-03:00"
}'
```
### 2. Verificar alertas pendentes
```bash
curl http://localhost:8000/api/v1/alerts?status=pending \
-H "Authorization: Bearer <jwt_token>"
```
### 3. Decidir sobre um alerta
```bash
curl -X POST http://localhost:8000/api/v1/alerts/<alert_id>/decide \
-H "Authorization: Bearer <jwt_token>" \
-H "Content-Type: application/json" \
-d '{"decision": "confirmed"}'
```
### 4. Criar uma regra
```bash
curl -X POST http://localhost:8000/api/v1/rules \
-H "Authorization: Bearer <admin_jwt_token>" \
-H "Content-Type: application/json" \
-d '{
"name": "Valor noturno elevado",
"description": "Detecta transações acima de R$ 1000 entre 22h e 06h",
"condition": "amount > 1000 and hour >= 22 or hour <= 6",
"weight": 25
}'
```
### 5. Testar regra contra dados históricos
```bash
curl -X POST http://localhost:8000/api/v1/rules/<rule_id>/test \
-H "Authorization: Bearer <admin_jwt_token>" \
-H "Content-Type: application/json" \
-d '{"days_back": 90}'
```
### 6. Dashboard
```bash
curl http://localhost:8000/api/v1/dashboard/metrics?period=24h \
-H "Authorization: Bearer <jwt_token>"
```
## Estrutura do Projeto
```
fraudshield/
├── backend/
│ ├── fraudshield/
│ │ ├── main.py # FastAPI app + routers
│ │ ├── config.py # Settings via pydantic-settings
│ │ ├── models/ # SQLAlchemy models
│ │ │ ├── transaction.py
│ │ │ ├── rule.py
│ │ │ ├── alert.py
│ │ │ └── audit.py
│ │ ├── engine/ # Rule engine
│ │ │ ├── evaluator.py # Core evaluation logic
│ │ │ ├── rules/ # Individual rule implementations
│ │ │ └── explainer.py # Explanation generation
│ │ ├── api/ # Route handlers
│ │ │ ├── transactions.py
│ │ │ ├── alerts.py
│ │ │ ├── rules.py
│ │ │ └── dashboard.py
│ │ ├── services/ # Business logic
│ │ ├── auth.py # JWT + OAuth2
│ │ └── seed.py # Dev data seeder
│ ├── alembic/ # DB migrations
│ ├── tests/
│ │ ├── unit/
│ │ ├── integration/
│ │ └── contract/
│ └── pyproject.toml
├── frontend/
│ ├── src/
│ │ ├── components/
│ │ ├── pages/
│ │ ├── hooks/
│ │ ├── api/ # Generated API client
│ │ └── App.tsx
│ ├── tests/
│ └── package.json
├── docker-compose.yml
└── specs/
```
## Regras Iniciais (Seed)
O seed popula 5 regras padrão:
| Regra | Condição | Peso |
|-------|----------|------|
| Valor atípico | `amount > customer.avg_amount_30d * 3` | 30 |
| Local incomum | `distance(current_location, customer.home_location) > 100km` | 25 |
| Horário suspeito | `hour >= 0 and hour <= 5 and amount > 500` | 20 |
| Categoria rara | `merchant_category not in customer.common_categories` | 15 |
| Canal novo | `channel not in customer.common_channels` | 10 |
Threshold padrão do sistema: **70/100**.

View file

@ -0,0 +1,127 @@
# Research: Motor de Detecção de Fraudes Bancárias
**Feature**: 001-fraud-detection-engine
**Date**: 2026-05-11
**Status**: Complete
## 1. Backend Runtime & Framework
**Decision**: Python 3.12 + FastAPI
**Rationale**:
- Python é a linguagem dominante em data science e financial services — facilita contratação e evolução futura para ML.
- FastAPI oferece async nativo, validação automática via Pydantic, OpenAPI auto-generated, e performance comparável a Node.js.
- Ecossistema rico para processamento de dados (pandas, numpy) útil para análise de transações e backtesting de regras.
- Tipagem estática com mypy alinha-se com o princípio de Test-First da constituição.
**Alternatives considered**:
- **Node.js + Express**: Bom para I/O, mas ecossistema de data processing inferior. Regras complexas seriam mais verbosas.
- **Go**: Performance superior, mas ecossistema de dados limitado e curva de aprendizado maior para analistas que contribuem com regras.
- **Java + Spring Boot**: Maduro para banking, mas verboso e lento para iteração. Overkill para MVP.
## 2. Frontend Framework
**Decision**: React 19 + TypeScript + Tailwind CSS
**Rationale**:
- Dashboard com múltiplos gráficos e filtros se beneficia do modelo de componentes React.
- TypeScript alinha-se com a tipagem do backend (Pydantic → TypeScript types via OpenAPI codegen).
- Tailwind CSS para velocidade de desenvolvimento e consistência visual sem CSS complexo.
- Recharts ou Tremor para gráficos de dashboard (nativos React, bem mantidos).
**Alternatives considered**:
- **HTMX + templates Jinja2**: Mais simples, mas dashboard interativo com filtros dinâmicos e real-time updates seria complexo.
- **Vue 3 + Nuxt**: Excelente framework, mas React tem ecossistema maior de componentes de dashboard.
- **Svelte**: Performance superior, mas ecossistema de componentes para dashboard ainda imaturo comparado a React.
## 3. Database
**Decision**: PostgreSQL 16
**Rationale**:
- Audit log imutável: PostgreSQL com triggers ou particionamento por mês atende sem complexidade adicional.
- JSONB para payload flexível das transações e regras.
- Full-text search nativo para busca em explicações.
- Window functions para cálculos de médias móveis (baseline do cliente).
- `pgAudit` extension para camada extra de auditoria a nível de banco.
- Row-Level Security como defesa em profundidade.
**Alternatives considered**:
- **MongoDB**: Schema flexível para transações, mas audit log imutável é mais natural em relacional. Transações ACID são essenciais para dados financeiros.
- **SQLite**: Ótimo para dev/local, insuficiente para concorrência em produção.
- **TimescaleDB (extensão PostgreSQL)**: Considerar para hypertables de transações se volume crescer além de 100k/dia. Não necessário para MVP.
## 4. Cache & Real-Time
**Decision**: Redis 7
**Rationale**:
- Contadores em tempo real do dashboard (transações/minuto, alertas/24h) via Redis `INCR` + TTL.
- Cache de médias do cliente (gasto médio, localização habitual) para o motor de regras (evita JOIN pesado a cada transação).
- Rate limiting da API de ingestão.
- Pub/Sub para notificações de novos alertas no dashboard (SSE ou WebSocket).
**Alternatives considered**:
- **Sem cache (query direta no PostgreSQL)**: Viável para MVP com <10k transações/dia, mas dashboard near real-time sofreria.
- **Memcached**: Mais simples, mas sem Pub/Sub e estruturas de dados avançadas.
## 5. Message Queue (Picos de Volume)
**Decision**: Redis Streams (MVP) → RabbitMQ (scale)
**Rationale**:
- Para MVP, Redis Streams oferece consumer groups, acknowledgments, e já está na stack.
- Se volume crescer, migrar para RabbitMQ que oferece dead letter queues, retry policies, e routing complexo.
- Padrão: API de ingestão → Redis Streams → Workers de processamento → PostgreSQL.
- Desacopla ingestão do processamento, essencial para os picos de 5x mencionados na spec.
**Alternatives considered**:
- **Kafka**: Overkill para MVP. Operação complexa.
- **Processamento síncrono**: Violaria SC-001 (2s p95) durante picos.
## 6. Rule Engine Architecture
**Decision**: Python rules engine built on Pydantic models + expression evaluator
**Rationale**:
- Regras são condições booleanas com peso. Exemplo: `transaction.amount > customer.avg_amount * 3` → score 30.
- Pydantic models definem o schema da regra, validação automática.
- `simpleeval` ou `lark-parser` para avaliar expressões de forma segura (sem `eval()` nativo).
- Cada regra é uma classe Python que implementa `evaluate(transaction, context) -> RuleResult`.
- Conjunto de regras é carregado do PostgreSQL no boot e cacheado em memória. Reload via endpoint admin.
- Snapshot versionado: cada transação registra qual versão do ruleset foi usada (rastreabilidade → FR-005).
**Alternatives considered**:
- **Drools (Java)**: Maduro mas exige JVM, complexidade desnecessária.
- **Django Rules**: Acoplado ao Django ORM, não adequado para FastAPI.
- **JSONLogic**: Bom para regras simples, limitado para expressões aritméticas complexas.
## 7. Testing Strategy
**Decision**: pytest + pytest-asyncio (backend), Vitest + React Testing Library (frontend)
**Rationale**:
- TDD mandatório por constituição.
- Testes de regra: dataset de transações com fraudes conhecidas → assert score, assert explicação, assert alerta.
- Testes de contrato: OpenAPI schema validation, garantir que API não quebra entre versões.
- Testes de integração: pipeline completo (API → rules → alerta → decisão).
- Testes E2E: Playwright para fluxos do analista (login → fila → avalia alerta → decide).
## 8. Authentication & Authorization
**Decision**: JWT + OAuth2/OIDC via external IdP (Keycloak ou Auth0)
**Rationale**:
- Spec assume identity provider externo. FastAPI tem suporte nativo a OAuth2 + JWT.
- Perfis (analista, admin, sênior) mapeados para claims no token.
- Middleware de autorização por rota verifica claims.
- Sem senhas armazenadas no FraudShield — compliance LGPD simplificada.
## 9. Deployment
**Decision**: Docker Compose (dev/local) → Kubernetes ou cloud PaaS (produção)
**Rationale**:
- Docker Compose para ambiente dev: PostgreSQL + Redis + API + Frontend em containers.
- Simplicidade para MVP: um `docker compose up` resolve.
- Produção depende de onde hospedar (AWS ECS, GCP Cloud Run, Railway, etc.). Fora do escopo do MVP decidir.