Pular para conteúdo

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) + 1closing_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/spending mantido como alias delegando a generate_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 retorna general.

8. Migration rules (regra do repos/auraxis-api/CLAUDE.md)

  • Todas colunas novas em cc1_credit_card_extension.py nullable ou com server_default (sem backfill necessário).
  • Qualquer enum novo: native_enum=False + comentário de justificativa.
  • bash scripts/test_migrations_local.sh round-trip antes do push.
  • ENRICHMENT em scripts/openapi_to_postman.py na mesma PR para POST /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-cards aceita bank, description, benefits[], validity_date com validação completa
  • PUT /credit-cards/<id> atualiza qualquer subconjunto sem perder campos não enviados
  • GET retorna benefits como 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 benefits array, validityDate ISO

% limite utilizado

  • GET /credit-cards/<id>/utilization retorna {committed_amount, available_amount, limit_amount, utilization_pct, cycle}
  • Status cancelled e postponed excluídos do committed
  • Quando limit_amount é null, utilization_pct retorna null
  • 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-MM retorna ciclo + transações + totais
  • 404 quando cartão pertence a outro user
  • GraphQL creditCardBill(cardId, month) paridade
  • Web: página /settings/credit-cards/<id>/bill com month picker
  • App: screen app/(stack)/credit-cards/[id]/bill.tsx

Form de transação expõe cartão + parcelas

  • Web: QuickTransactionForm mostra card selector + installment toggle + count input quando type === "expense", sem prop gating
  • Web: preview 12x de R$ 100,00 a partir de 17/05/2026 até 17/04/2027 quando is_installment true e count válido
  • App: form expõe creditCardId, isInstallment, installmentCount com Zod refinement obrigatório
  • Após submit com 12 parcelas → 12 transactions com mesmo installment_group_id
  • Transaction detail mostra Parcela 3/12 quando aplicável

Insights globais com dimensões

  • POST /ai/insights/generate {period_type, anchor_date} retorna 1 row AIInsight com content.items[].dimension
  • Quota 2x/dia compartilhada com /ai/insights/spending
  • Comparações previous_day, previous_week, previous_month, same_day_previous_year presentes quando há dado, omitidas quando não
  • Snapshot inclui section credit_cards com utilization + bill cycles
  • GraphQL generateAiInsight paridade
  • 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 /insights lista insights agrupados por dimensão (general primeiro, depois transactions, credit_cards, goals, budgets)
  • Web /transactions, /settings/credit-cards, /goals, /budgets mostram apenas itens da própria dimensão (+ general)
  • App app/(tabs)/insights.tsx mesma estrutura
  • Geração disparada de qualquer surface consome a mesma quota
  • Hit cache (segunda chamada mesmo dia) retorna cached:true sem consumir quota
  • Terceira chamada no dia retorna 429

Observabilidade

  • Counter ai_insight_generated_total{period_type,dimension} exposto no /metrics
  • Histograma ai_insight_tokens com p50/p95/p99
  • Snapshot > 12 KiB truncado deterministicamente com log ai_advisory.snapshot.truncated
  • AIInsight.metadata_json armazena snapshot_version + comparisons_available[]
  • ADR docs/adr/0005-ai-insight-dimensions.md documentando 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_itemsLLMProviderError 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

  1. Backend Sprint 1: bash scripts/run_ci_quality_local.sh --local + bash scripts/test_migrations_local.sh
  2. Backend Sprint 3: idem + node scripts/openapi_to_postman.js && newman run … (após ENRICHMENT)
  3. Backend Sprint 5: idem + métrica visível em /metrics local
  4. Frontend Sprint 2 (Codex): pnpm quality-check + pnpm test app/features/credit-cards; npm run quality-check
  5. Frontend Sprint 4 (Codex): idem + smoke manual: criar 12x parcelas, gerar insight da tela de cartões, confirmar dimensão filtrada
  6. Smoke E2E pós-Sprint 4:
  7. Criar parcelamento 12x na web → 12 rows na fatura do cartão correspondente
  8. Disparar "Gerar insights" da tela de cartões → 1 record persistida com itens em múltiplas dimensões
  9. Confirmar que /settings/credit-cards mostra só itens credit_cards + general, e /insights mostra todos agrupados por dimensão
  10. 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)