feat(#35): rendimento CDI automático em contas de investimento

- Migration 010: colunas yield_type e last_yield_date em accounts + source='yield' nas transações
- CDIYieldService: busca taxas BCB (serie 12) e cria transação source=yield com rendimento acumulado
- Weekends/feriados sem taxa usam última taxa disponível (AC4)
- GET /api/accounts dispara cálculo CDI on-demand antes de retornar saldos
- AccountsView: seletor yield_type (none/cdi/variable) + badge CDI/VAR na listagem
- Transações source=yield aparecem no histórico com descrição 'Rendimento CDI — {período}'

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
2026-05-27 15:06:05 -03:00
co-authored by Claude Sonnet 4.6
parent 79f4a62a1b
commit ee6cd9f37e
9 changed files with 249 additions and 27 deletions
+3
View File
@@ -8,6 +8,8 @@ export interface Account {
type: 'checking' | 'savings' | 'investment' | 'credit'
initial_balance: number
balance: number
yield_type: 'none' | 'cdi' | 'variable'
last_yield_date?: string
created_at: string
updated_at: string
}
@@ -16,6 +18,7 @@ export interface AccountInput {
name: string
type: string
initial_balance: number
yield_type: string
}
export const useAccountsStore = defineStore('accounts', () => {
+22 -5
View File
@@ -13,7 +13,7 @@ const typeLabels: Record<string, string> = {
credit: 'Cartão de Crédito',
}
const blank = (): AccountInput => ({ name: '', type: 'checking', initial_balance: 0 })
const blank = (): AccountInput => ({ name: '', type: 'checking', initial_balance: 0, yield_type: 'none' })
const form = ref(blank())
const editId = ref<number | null>(null)
const initialRaw = ref('')
@@ -27,7 +27,7 @@ function startEdit(id: number) {
const a = store.accounts.find((x) => x.id === id)
if (!a) return
editId.value = id
form.value = { name: a.name, type: a.type, initial_balance: a.initial_balance }
form.value = { name: a.name, type: a.type, initial_balance: a.initial_balance, yield_type: a.yield_type ?? 'none' }
initialRaw.value = a.initial_balance.toLocaleString('pt-BR', { minimumFractionDigits: 2 })
formError.value = null
}
@@ -85,6 +85,11 @@ function totalBalance() {
<option value="investment">Investimento</option>
<option value="credit">Cartão de Crédito</option>
</select>
<select v-model="form.yield_type" class="fc-select fc-acc-form__yield">
<option value="none">Sem rendimento</option>
<option value="cdi">CDI automático</option>
<option value="variable">Renda variável</option>
</select>
<input
v-model="initialRaw"
placeholder="Saldo inicial (ex: 1.500,00)"
@@ -120,7 +125,11 @@ function totalBalance() {
:class="{ 'fc-acc-item--editing': editId === a.id }"
>
<div class="fc-acc-item__info">
<span class="fc-body fc-acc-item__name">{{ a.name }}</span>
<div class="fc-acc-item__name-row">
<span class="fc-body fc-acc-item__name">{{ a.name }}</span>
<span v-if="a.yield_type === 'cdi'" class="fc-acc-badge fc-acc-badge--cdi fc-pixel">CDI</span>
<span v-else-if="a.yield_type === 'variable'" class="fc-acc-badge fc-acc-badge--var fc-pixel">VAR</span>
</div>
<span class="fc-mono fc-acc-item__meta">
{{ typeLabels[a.type] }} · inicial {{ fmt(a.initial_balance) }}
</span>
@@ -165,8 +174,9 @@ function totalBalance() {
}
.fc-acc-form__name { flex: 1; min-width: 160px; }
.fc-acc-form__type { width: 160px; }
.fc-acc-form__balance { width: 160px; }
.fc-acc-form__type { width: 150px; }
.fc-acc-form__yield { width: 160px; }
.fc-acc-form__balance { width: 150px; }
.fc-acc-form__error {
color: var(--fc-red);
@@ -237,9 +247,16 @@ function totalBalance() {
gap: 4px;
}
.fc-acc-item__name-row { display: flex; align-items: center; gap: 8px; }
.fc-acc-item__name { font-size: 14px; font-weight: 500; }
.fc-acc-item__meta { font-size: 11px; color: var(--fc-text-dim); }
.fc-acc-badge {
font-size: 7px; padding: 2px 5px; border-radius: 2px; letter-spacing: .05em;
}
.fc-acc-badge--cdi { background: rgba(34,197,94,.2); color: var(--fc-green); border: 1px solid var(--fc-green); }
.fc-acc-badge--var { background: rgba(251,191,36,.2); color: var(--fc-gold); border: 1px solid var(--fc-gold); }
.fc-acc-item__right {
display: flex;
align-items: center;