feat: add guarded savings agent tools
This commit is contained in:
225
docs/superpowers/plans/2026-06-23-kontostandabruf-ui.md
Normal file
225
docs/superpowers/plans/2026-06-23-kontostandabruf-ui.md
Normal file
@@ -0,0 +1,225 @@
|
||||
# Kontostandabruf und UI-Anzeige Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Build a dedicated live balance refresh path for FinTS and comdirect REST, persist latest balances separately from account master data, and show them clearly in the dashboard and account UI.
|
||||
|
||||
**Architecture:** Keep current transaction sync intact and add a separate balance capability. Persist balance snapshots in a dedicated Convex table because live balances are operational/high-churn data, while `accounts` remains stable account metadata. The UI reads the latest persisted balances through a query and can trigger a refresh action that uses provider preference `auto | comdirect | fints` with the same fallback behavior as the current sync.
|
||||
|
||||
**Tech Stack:** React 19, Vite, Convex 1.41, `lib-fints`, comdirect REST client, Vitest, convex-test.
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
- Backlog task: `TASK-14 Plane Kontostandabruf und UI-Anzeige`.
|
||||
- Existing balance-capable types already exist in `convex/bank/types.ts`: `NormalizedAccount.balance` and `NormalizedBalance`.
|
||||
- Existing comdirect balance endpoint wrapper exists in `convex/comdirect/client.ts`: `getAccountBalances()`.
|
||||
- Existing FinTS balance logic exists inside transaction sync in `convex/bank/orchestrator.ts`: `client.canGetAccountBalance()` and `client.getAccountBalance()`.
|
||||
- Current persistence only uses `balance` as `openingBalance` when creating a new account; it does not store or display latest live balance.
|
||||
- `ctx7` found FinTS docs for `/nemiah/phpfints`; relevant notes: FinTS/HBCI supports account access and balance retrieval, and real deployments need a registered product/application id. `ctx7` did not find a usable comdirect API reference, so implementation should rely on the existing local comdirect client wrappers and verify against real responses.
|
||||
|
||||
## Data Model
|
||||
|
||||
### Task 1: Store Latest Account Balances
|
||||
|
||||
**Files:**
|
||||
- Modify: `convex/schema.ts`
|
||||
- Modify: `convex/bank/internal.ts`
|
||||
- Test: `convex/bank/balances.test.ts`
|
||||
|
||||
- [ ] Add `accountBalances` table in `convex/schema.ts`:
|
||||
|
||||
```ts
|
||||
accountBalances: defineTable({
|
||||
userId: v.id("users"),
|
||||
accountId: v.id("accounts"),
|
||||
externalId: v.string(),
|
||||
provider: v.union(v.literal("comdirect"), v.literal("fints")),
|
||||
balance: v.number(),
|
||||
currency: v.string(),
|
||||
asOf: v.optional(v.string()),
|
||||
fetchedAt: v.number(),
|
||||
status: v.union(v.literal("fresh"), v.literal("stale"), v.literal("error")),
|
||||
errorMessage: v.optional(v.string()),
|
||||
})
|
||||
.index("by_user", ["userId"])
|
||||
.index("by_user_account", ["userId", "accountId"])
|
||||
.index("by_user_provider_fetched", ["userId", "provider", "fetchedAt"]),
|
||||
```
|
||||
|
||||
- [ ] Add `upsertAccountBalance` internal mutation in `convex/bank/internal.ts`.
|
||||
- [ ] Add `listLatestAccountBalances` internal query that returns one latest balance per account for the authenticated user's accounts.
|
||||
- [ ] Keep `accounts.openingBalance` unchanged; do not repurpose it as live balance.
|
||||
- [ ] Test that inserting a newer balance replaces the displayed latest balance without mutating `accounts.openingBalance`.
|
||||
|
||||
## Provider Flow
|
||||
|
||||
### Task 2: Normalize Provider Balance Fetching
|
||||
|
||||
**Files:**
|
||||
- Modify: `convex/bank/types.ts`
|
||||
- Modify: `convex/bank/comdirectProvider.ts`
|
||||
- Modify: `convex/bank/orchestrator.ts`
|
||||
- Test: `convex/bank/balanceProviders.test.ts`
|
||||
|
||||
- [ ] Extend `NormalizedBalance` with optional `asOf?: string`.
|
||||
- [ ] Keep comdirect `getBalance(accountExternalId)` as the per-account lookup, but add a bulk path that maps `/api/banking/clients/user/v2/accounts/balances` into `NormalizedBalance[]`.
|
||||
- [ ] Extract FinTS balance fetching from `fetchFinTsAccountData()` into a reusable helper:
|
||||
|
||||
```ts
|
||||
async function fetchFinTsAccountBalance(
|
||||
ctx: ActionCtx,
|
||||
userId: Id<"users">,
|
||||
client: FinTSClient,
|
||||
account: NormalizedAccount,
|
||||
syncJob: PendingSyncJob,
|
||||
): Promise<NormalizedBalance>
|
||||
```
|
||||
|
||||
- [ ] If FinTS reports `!client.canGetAccountBalance(account.externalId)`, return an error result for that account instead of failing the whole refresh.
|
||||
- [ ] Preserve TAN handling by reusing `resolveTanResponse()` for balance requests.
|
||||
- [ ] Test provider selection:
|
||||
- `providerPreference: "fints"` uses FinTS only.
|
||||
- `providerPreference: "comdirect"` uses REST and only falls back if the current fallback rules say the error is recoverable.
|
||||
- `providerPreference: "auto"` tries REST when configured, otherwise FinTS.
|
||||
|
||||
## Convex Public API
|
||||
|
||||
### Task 3: Add Balance Query and Refresh Action
|
||||
|
||||
**Files:**
|
||||
- Create: `convex/bank/balances.ts`
|
||||
- Modify: `convex/bank/orchestrator.ts`
|
||||
- Modify: `convex/bank/internal.ts`
|
||||
- Test: `convex/bank/balances.test.ts`
|
||||
|
||||
- [ ] Add public query `api.bank.balances.listLatest`.
|
||||
|
||||
Return shape:
|
||||
|
||||
```ts
|
||||
{
|
||||
accountId: Id<"accounts">;
|
||||
accountName: string;
|
||||
accountType: string;
|
||||
iban?: string;
|
||||
externalId?: string;
|
||||
balance: number | null;
|
||||
currency: string;
|
||||
provider: "comdirect" | "fints" | null;
|
||||
fetchedAt: number | null;
|
||||
asOf?: string;
|
||||
status: "fresh" | "stale" | "error" | "missing";
|
||||
errorMessage?: string;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] Add public action `api.bank.balances.refresh`.
|
||||
|
||||
Arguments:
|
||||
|
||||
```ts
|
||||
{
|
||||
accountId?: Id<"accounts">;
|
||||
pin?: string;
|
||||
}
|
||||
```
|
||||
|
||||
Return shape:
|
||||
|
||||
```ts
|
||||
{
|
||||
updatedCount: number;
|
||||
provider: "comdirect" | "fints";
|
||||
awaitingTan: boolean;
|
||||
errors: Array<{ accountId?: Id<"accounts">; externalId?: string; message: string }>;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] Add internal action `refreshBalancesInternal` in `convex/bank/orchestrator.ts`, reusing provider preference and fallback logic from `runSyncInternal`.
|
||||
- [ ] When transaction sync already fetches balances, call `upsertAccountBalance` during `persistSyncResults()` so a full sync also updates UI balances.
|
||||
- [ ] Keep this as a Convex action/query API first; add an HTTP route in `convex/http.ts` only if an external scheduler or webhook needs it.
|
||||
|
||||
## UI
|
||||
|
||||
### Task 4: Add Balance UI Components
|
||||
|
||||
**Files:**
|
||||
- Create: `src/components/accounts/AccountBalanceStrip.tsx`
|
||||
- Modify: `src/pages/DashboardPage.tsx`
|
||||
- Modify: `src/pages/SettingsPage.tsx`
|
||||
- Optional modify: `src/components/import/ComdirectSyncPanel.tsx`
|
||||
- Test: `src/components/accounts/AccountBalanceStrip.test.tsx`
|
||||
|
||||
- [ ] Create `AccountBalanceStrip` that calls `api.bank.balances.listLatest`.
|
||||
- [ ] Show compact account rows with:
|
||||
- account name
|
||||
- IBAN tail or type
|
||||
- live balance
|
||||
- provider badge
|
||||
- fetched timestamp
|
||||
- stale/error/missing state
|
||||
- [ ] Add refresh button with `RefreshCw` icon from `lucide-react`.
|
||||
- [ ] Put the strip near the top of `DashboardPage`, above the period-based KPI cards. This avoids confusing live bank balance with the existing filtered `Saldo` KPI.
|
||||
- [ ] In `SettingsPage`, show the latest live balance in each account row next to account metadata.
|
||||
- [ ] For stale data, use subdued text and a warning badge; for errors, show the last known balance plus the error message.
|
||||
- [ ] Keep cards shallow; do not nest balance cards inside other cards.
|
||||
|
||||
## UX Rules
|
||||
|
||||
- Label live bank balance as `Aktueller Kontostand`.
|
||||
- Keep existing dashboard `Saldo` label as period result, or rename it to `Periodensaldo` to avoid ambiguity.
|
||||
- Show `Noch nicht abgerufen` for accounts without a balance snapshot.
|
||||
- Show `Aktualisiert vor ...` or localized timestamp for fetched balances.
|
||||
- If refresh returns `awaitingTan`, reuse the existing TAN dialog/status flow rather than adding a second TAN UX.
|
||||
- If comdirect REST fails and FinTS succeeds, show a non-blocking toast: `comdirect REST fehlgeschlagen, FinTS verwendet`.
|
||||
|
||||
## Testing
|
||||
|
||||
### Task 5: Verify Backend Behavior
|
||||
|
||||
**Files:**
|
||||
- Create: `convex/bank/balances.test.ts`
|
||||
- Create or extend: `convex/bank/balanceProviders.test.ts`
|
||||
|
||||
- [ ] Use `convex-test` with `edge-runtime`, matching existing project guidance.
|
||||
- [ ] Test latest-balance query for `fresh`, `missing`, and `error` states.
|
||||
- [ ] Test that archived accounts are either hidden by default or marked clearly; choose hidden for dashboard and visible in settings.
|
||||
- [ ] Test full sync calls balance persistence for both providers.
|
||||
- [ ] Test balance-only refresh does not import transactions.
|
||||
|
||||
### Task 6: Verify Frontend Behavior
|
||||
|
||||
**Files:**
|
||||
- Create: `src/components/accounts/AccountBalanceStrip.test.tsx`
|
||||
- Modify: `src/pages/DashboardPage.tsx`
|
||||
|
||||
- [ ] Test loading skeleton.
|
||||
- [ ] Test missing state.
|
||||
- [ ] Test fresh EUR formatting with existing `formatAmount`.
|
||||
- [ ] Test error state keeps last known balance visible.
|
||||
- [ ] Test refresh button disables while the action is running.
|
||||
|
||||
## Execution Order
|
||||
|
||||
1. Add schema and internal persistence.
|
||||
2. Add backend query tests and make them fail.
|
||||
3. Implement internal balance mutations/queries.
|
||||
4. Extract provider balance helpers.
|
||||
5. Add refresh action and wire provider fallback.
|
||||
6. Update transaction sync to persist balances opportunistically.
|
||||
7. Add UI component and Dashboard placement.
|
||||
8. Add Settings account-row balance display.
|
||||
9. Run `pnpm lint`.
|
||||
10. Run `pnpm build`.
|
||||
11. Run targeted Vitest suites.
|
||||
|
||||
## Manual Verification
|
||||
|
||||
1. Set provider preference to `fints`, enter FinTS PIN if not in Convex env, refresh balances.
|
||||
2. Confirm dashboard shows `Aktueller Kontostand` per account.
|
||||
3. Confirm existing `Saldo`/`Periodensaldo` still follows the selected date range.
|
||||
4. Set provider preference to `auto` with broken comdirect REST credentials and valid FinTS config; confirm fallback works.
|
||||
5. Run a normal transaction sync and confirm balances update without pressing the balance refresh button separately.
|
||||
|
||||
Reference in New Issue
Block a user