MVP 3 — Hub de Cartões de Crédito + Insights de IA Globais¶
Atualizado: 2026-05-17
Visão¶
Construir um Hub de Cartões de Crédito completo (CRUD enriquecido, vinculação a transações, parcelamento com fan-out em débitos futuros, % limite utilizado, fatura mensal) e estender o motor de AI Insights para emitir uma única record contendo itens segmentados por dimensão (general | transactions | credit_cards | goals | budgets).
A página contextual de cada superfície (transações, cartões, metas, orçamentos) exibe apenas os itens da sua dimensão, enquanto o hub /insights exibe todos agrupados. O usuário tem quota global de 2 gerações/dia (Premium), independente da tela que disparou. Cada geração compara o estado atual com janelas temporais anteriores (insight anterior, dia anterior, semana anterior, mês anterior, mesmo dia do ano passado quando há histórico).
Este MVP supersedes a entrega prévia de MVP-2-AI-Insights-Financial-Snapshot.md para todo o caminho de geração (item shape, endpoint, persistência de comparações). O snapshot determinístico backend-first permanece o mesmo princípio, expandido em dimensões.
Por que agora¶
Inventário cruzado dos 4 repos mostra que ~60% da fundação já existe:
| Bloco | Status |
|---|---|
CreditCard model + CRUD REST + Marshmallow (v1 Flask) |
✅ pronto |
Transaction.credit_card_id, is_installment, installment_count, installment_group_id |
✅ pronto |
Fan-out de parcelamento (cria N rows com mesmo installment_group_id) — app/application/services/transaction/writes.py:80-107 |
✅ pronto |
AIInsight persistido com previous_insight_id (chain temporal) |
✅ pronto |
Rate limit 2x/dia Premium — app/middleware/ai_rate_limit.py |
✅ pronto |
LGPD consent + LLMAuditLog redacted |
✅ pronto |
Spec financial_insight_snapshot.v1 com current_period + comparisons + daily_series |
✅ pronto |
Form web tem showCreditCard/showInstallment* props (gated, escondidos por default) |
⚠️ gated |
| Form mobile não expõe card/parcelas | ❌ falta |
| Campos extras CreditCard (bank, description, benefits, validity_date, timestamps) | ❌ falta |
| % limite utilizado calculado server-side | ❌ falta |
| Fatura mensal por cartão (bill cycle) | ❌ falta |
| GraphQL CreditCard types + queries | ❌ falta |
dimension em itens de insight |
❌ falta |
| Comparações vs dia/semana/mês/mesmo-dia-ano-passado | ❌ falta |
Diretiva técnica¶
Backend target: auraxis-api (Flask v1). Confirmado em chat 2026-05-17. Migração v2 (FastAPI) fora de escopo deste MVP.
Frontend ownership: cards de frontend (web + app) serão executados pelo Codex. Cards de backend executados por Claude.
Decisões arquiteturais¶
1. CreditCard model — campos novos¶
| Campo | Tipo | Notas |
|---|---|---|
bank |
String(80) |
Opcional. Texto livre (ex: "Nubank", "Itaú") |
description |
String(300) |
Opcional. Notas livres do usuário |
benefits |
Text (JSON-encoded list) |
Opcional. Lista de strings, cap 12 itens × 120 chars. JSON-encoded para compat SQLite nos testes. |
validity_date |
Date |
Opcional. Validade do cartão físico (MM/YYYY armazenado como Date primeiro dia do mês) |
created_at |
DateTime |
server_default=now() |
updated_at |
DateTime |
server_default=now(), onupdate=now() |
Por que benefits como JSON-Text e não array PG: o repo tem testes em SQLite que rodam em CI. Array PostgreSQL não é portável. JSON em Text mantém ambos compatíveis e segue o padrão de AIInsight.content.
2. Bill cycle (ciclo de fatura)¶
Service novo app/services/credit_card_bill_service.py:
@dataclass(frozen=True)
class BillCycle:
start_date: date # primeiro dia do ciclo aberto atual
end_date: date # dia de fechamento (closing_day) do ciclo
due_date: date # vencimento da fatura
status: Literal["open", "closed", "paid"]
def compute_bill_cycle(closing_day: int, due_day: int, anchor: date) -> BillCycle: ...
def compute_bill(card: CreditCard, month: str) -> BillSummary: ...
def compute_utilization(card: CreditCard, today: date) -> Utilization: ...
Regra do ciclo aberto: janela atual aberta = (closing_day de M-1) + 1 → closing_day de M. Due date = due_day de M+1 quando due_day < closing_day, caso contrário M.
Edge cases obrigatórios em testes:
- closing=10 due=15 mesmo mês (due_day > closing_day)
- closing=25 due=5 mês seguinte (due_day < closing_day)
- Ano bissexto (Feb 29 como anchor ou end_date)
- Anchor dentro do ciclo vs anchor fora (depois do due_date)
- Status transições open → closed → paid
3. Utilização % — definição canônica¶
committed = sum(amount) WHERE
credit_card_id = X
AND type = EXPENSE
AND status IN (pending, overdue, paid)
AND due_date BETWEEN cycle.start_date AND cycle.end_date
available = limit_amount - committed
utilization_pct = round(committed / limit_amount * 100, 1) # null se limit_amount null
Status cancelled e postponed não contam. Retornado como Decimal string nos contratos REST + GraphQL.
4. Insight dimension — enum fechado¶
class InsightDimension(str, Enum):
GENERAL = "general"
TRANSACTIONS = "transactions"
CREDIT_CARDS = "credit_cards"
GOALS = "goals"
BUDGETS = "budgets"
Cada item gerado pelo LLM tem campo dimension. Validado contra o enum no JSON-schema strict mode da OpenAI. Em caso de valor inválido, _coerce_spending_insight_items levanta LLMProviderError.
Legacy rows (anteriores a este MVP) sem dimension no content → coerção retorna dimension="general" (compatibility fallback, coberto por unit test).
5. Quota global 2x/dia¶
Decidida pelo PO em chat: 2 gerações por dia por usuário, total, independente da tela. Não há rate limit por escopo. Cache idempotente continua funcionando: segunda chamada no mesmo period_label retorna cached:true sem consumir quota.
POST /ai/insights/generate compartilha o mesmo Redis key namespace de GET /ai/insights/spending (alias). O decorator @ai_daily_limit() é aplicado em ambos.
6. Comparações temporais¶
A FinancialInsightContextBuilder produz snapshot com bloco comparisons:
{
"comparisons": {
"previous_insight": { "period_label": "2026-05-16", "expense_total": "320.00" },
"previous_day": { "expense_total": "180.50" },
"previous_week": { "expense_total": "1450.00" },
"previous_month": { "expense_total": "5200.00" },
"same_day_previous_year": { "expense_total": "210.00" }
}
}
Regra de graceful degradation: quando não há dado histórico para preencher uma comparação (ex: usuário novo sem mesmo-dia-ano-passado), o campo é omitido do snapshot. O LLM recebe somente comparações com dado real.
7. Backward compatibility¶
GET /ai/insights/spendingmantido como alias delegando agenerate_insight(period_type="monthly").- Resposta REST legada preservada (mesmo shape
{insights, tokens_used, cost_usd, model, cached}). - Novo conteúdo persistido no DB adota o shape
items[].dimension+comparisons. - Rows legadas sem
dimension→ coerção retornageneral.
8. Migration rules (regra do repos/auraxis-api/CLAUDE.md)¶
- Todas colunas novas em
cc1_credit_card_extension.pynullable ou comserver_default(sem backfill necessário). - Qualquer enum novo:
native_enum=False+ comentário de justificativa. bash scripts/test_migrations_local.shround-trip antes do push.- ENRICHMENT em
scripts/openapi_to_postman.pyna mesma PR paraPOST /ai/insights/generate.
Sprints¶
Sprint 1 — Backend: CreditCard model + bill cycle + utilization + GraphQL¶
Repo: auraxis-api. PR único, ~700-1000 LOC. Cards:
- [BACKEND] cc-1 (#1284) Estender CreditCard model com bank/description/benefits/validity_date/timestamps
- [BACKEND] cc-2 (#1285) Bill cycle service + GET /credit-cards/<id>/bill
- [BACKEND] cc-3 (#1286) Utilization endpoint + GraphQL parity para CreditCard
Executor: Claude. Bloqueia: Sprint 2 (frontend) e Sprint 3 (insights builder).
Sprint 2 — Frontend: Hub de Cartões (web + app)¶
Repos: auraxis-web, auraxis-app. 2 PRs coordenados. Cards:
- [WEB] cc-4 Credit Cards Hub UI com extended fields + utilization bar + bill page
- [APP] cc-5 Credit Cards Hub UI parity em React Native
Executor: Codex. Depende: Sprint 1 deployed.
Sprint 3 — Backend: FinancialInsightContextBuilder + generate endpoint + items dimensionados¶
Repo: auraxis-api. PR único, ~600-800 LOC. Cards:
- [BACKEND] ai-1 FinancialInsightContextBuilder + dimensioned items (supersedes #1269, #1270)
- [BACKEND] ai-2 POST /ai/insights/generate + GraphQL generateAiInsight
Executor: Claude. Bloqueia: Sprint 4.
Sprint 4 — Frontend: Hub de Insights + form de transação com cartão e parcelas¶
Repos: auraxis-web, auraxis-app. 2 PRs coordenados. Cards:
- [WEB] ai-4 AI Insights Hub + contextual dimension filtering (supersedes #857)
- [WEB] tx-1 QuickTransactionForm ungated card+installments+preview
- [APP] ai-5 AI Insights screen + contextual filtering (supersedes #412)
- [APP] tx-2 Mobile transaction-form com creditCardId/isInstallment/installmentCount
Executor: Codex. Depende: Sprint 3 deployed.
Sprint 5 — Hardening: observabilidade + cost guardrails + ADR¶
Repo: auraxis-api. PR pequeno. Cards:
- [INFRA] obs-1 Prometheus metrics for AI insight generation + snapshot size + AIInsight.metadata_json
Executor: Claude. Não bloqueia: pode rodar em paralelo com Sprint 4.
Acceptance criteria — agregados por funcionalidade¶
CRUD enriquecido de cartões¶
- POST
/credit-cardsaceitabank,description,benefits[],validity_datecom validação completa - PUT
/credit-cards/<id>atualiza qualquer subconjunto sem perder campos não enviados - GET retorna
benefitscomo array JSON (não string) - DELETE preserva soft delete pattern do projeto
- Web: form do hub mostra todos os campos com mascaramento adequado
- App: form Zod com
benefitsarray,validityDateISO
% limite utilizado¶
-
GET /credit-cards/<id>/utilizationretorna{committed_amount, available_amount, limit_amount, utilization_pct, cycle} - Status
cancelledepostponedexcluídos docommitted - Quando
limit_amounté null,utilization_pctretornanull - GraphQL
creditCardUtilization(cardId)paridade - Web: lista de cartões mostra progress bar com label
Utilizado: 65% · R$ 3.250,00 / R$ 5.000,00 - App: card component com mesma progress bar e cor por faixa (verde <70%, amarelo 70-90%, vermelho >90%)
Fatura mensal por cartão¶
-
GET /credit-cards/<id>/bill?month=YYYY-MMretorna ciclo + transações + totais - 404 quando cartão pertence a outro user
- GraphQL
creditCardBill(cardId, month)paridade - Web: página
/settings/credit-cards/<id>/billcom month picker - App: screen
app/(stack)/credit-cards/[id]/bill.tsx
Form de transação expõe cartão + parcelas¶
- Web:
QuickTransactionFormmostra card selector + installment toggle + count input quandotype === "expense", sem prop gating - Web: preview
12x de R$ 100,00 a partir de 17/05/2026 até 17/04/2027quandois_installmenttrue e count válido - App: form expõe
creditCardId,isInstallment,installmentCountcom Zod refinement obrigatório - Após submit com 12 parcelas → 12 transactions com mesmo
installment_group_id - Transaction detail mostra
Parcela 3/12quando aplicável
Insights globais com dimensões¶
-
POST /ai/insights/generate {period_type, anchor_date}retorna 1 rowAIInsightcomcontent.items[].dimension - Quota 2x/dia compartilhada com
/ai/insights/spending - Comparações
previous_day,previous_week,previous_month,same_day_previous_yearpresentes quando há dado, omitidas quando não - Snapshot inclui section
credit_cardscom utilization + bill cycles - GraphQL
generateAiInsightparidade - Newman smoke test passa após ENRICHMENT
- LLM retorna dimension inválida →
LLMProviderError - Legacy row sem dimension → coerção retorna
general
Hub de Insights UI¶
- Web
/insightslista insights agrupados por dimensão (general primeiro, depois transactions, credit_cards, goals, budgets) - Web
/transactions,/settings/credit-cards,/goals,/budgetsmostram apenas itens da própria dimensão (+ general) - App
app/(tabs)/insights.tsxmesma estrutura - Geração disparada de qualquer surface consome a mesma quota
- Hit cache (segunda chamada mesmo dia) retorna
cached:truesem consumir quota - Terceira chamada no dia retorna 429
Observabilidade¶
- Counter
ai_insight_generated_total{period_type,dimension}exposto no/metrics - Histograma
ai_insight_tokenscom p50/p95/p99 - Snapshot > 12 KiB truncado deterministicamente com log
ai_advisory.snapshot.truncated -
AIInsight.metadata_jsonarmazenasnapshot_version+comparisons_available[] - ADR
docs/adr/0005-ai-insight-dimensions.mddocumentando a decisão de dimensão closed-enum
Risk register¶
| Risco | Mitigação |
|---|---|
| Snapshot explosivo → custo LLM | Cap 12 KiB com truncate determinístico de top_expenses + transactions[] + métrica ai_insight_tokens p95 no Grafana (Sprint 5) |
| Migration downtime | Todas colunas novas nullable ou com server_default; sem backfill |
| GraphQL parity drift | tests/test_graphql_auth_everywhere.py pega auth; adicionar assertion em tests/test_rest_graphql_parity.py para bill + utilization + generate |
| Mobile form breaking change | Validator-first TDD + feature flag ff:installments_v1 em features/bootstrap até smoke completo |
LLM retorna dimension inválida |
JSON-schema strict mode + _coerce_spending_insight_items → LLMProviderError ou fallback general |
Legacy AIInsight sem dimension |
Coerção default general coberta por unit test |
| Conflito com #1269, #1270, #1271, #857, #412 | Comentário de supersedes em cada e fechamento conforme PR de substituição mergeia |
Verificação end-to-end¶
- Backend Sprint 1:
bash scripts/run_ci_quality_local.sh --local+bash scripts/test_migrations_local.sh - Backend Sprint 3: idem +
node scripts/openapi_to_postman.js && newman run …(após ENRICHMENT) - Backend Sprint 5: idem + métrica visível em
/metricslocal - Frontend Sprint 2 (Codex):
pnpm quality-check+pnpm test app/features/credit-cards;npm run quality-check - Frontend Sprint 4 (Codex): idem + smoke manual: criar 12x parcelas, gerar insight da tela de cartões, confirmar dimensão filtrada
- Smoke E2E pós-Sprint 4:
- Criar parcelamento 12x na web → 12 rows na fatura do cartão correspondente
- Disparar "Gerar insights" da tela de cartões → 1 record persistida com itens em múltiplas dimensões
- Confirmar que
/settings/credit-cardsmostra só itenscredit_cards+general, e/insightsmostra todos agrupados por dimensão - Repetir disparo 3x no dia → segunda chamada
cached:true, terceira 429
Referências¶
- Plano executivo:
/Users/italochagas/.claude/plans/criar-um-hub-de-reactive-flurry.md - Spec snapshot v1 (background):
docs/superpowers/specs/2026-05-17-ai-insights-financial-snapshot-design.md - Wiki MVP-2 AI snapshot:
MVP-2-AI-Insights-Financial-Snapshot.md - ADR-0002 GraphQL ownership (paridade REST/GraphQL):
repos/auraxis-api/docs/adr/0002-graphql-ownership.md - ADR-0003 GraphQL flat types:
repos/auraxis-api/docs/adr/0003-graphql-flat-types-no-dataloader.md - ADR-0005 (a criar) AI Insight dimensions:
repos/auraxis-api/docs/adr/0005-ai-insight-dimensions.md
Cards (mapping)¶
| Card | Repo | Sprint | Executor | Status |
|---|---|---|---|---|
| cc-1 (#1284) | auraxis-api | 1 | Claude | open |
| cc-2 (#1285) | auraxis-api | 1 | Claude | open |
| cc-3 (#1286) | auraxis-api | 1 | Claude | open |
| cc-4 | auraxis-web | 2 | Codex | a abrir |
| cc-5 | auraxis-app | 2 | Codex | a abrir |
| ai-1 | auraxis-api | 3 | Claude | a abrir (supersedes #1269) |
| ai-2 | auraxis-api | 3 | Claude | a abrir (supersedes #1270) |
| ai-4 | auraxis-web | 4 | Codex | a abrir (supersedes #857) |
| tx-1 | auraxis-web | 4 | Codex | a abrir |
| ai-5 | auraxis-app | 4 | Codex | a abrir (supersedes #412) |
| tx-2 | auraxis-app | 4 | Codex | a abrir |
| obs-1 | auraxis-api | 5 | Claude | a abrir (supersedes #1271 parcial) |