Files
finanzen/docs/superpowers/plans/2026-06-23-kontostandabruf-ui.md
2026-06-23 21:13:02 +02:00

9.4 KiB

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:

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:

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:

{
  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:

{
  accountId?: Id<"accounts">;
  pin?: string;
}

Return shape:

{
  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.