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.balanceandNormalizedBalance. - 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()andclient.getAccountBalance(). - Current persistence only uses
balanceasopeningBalancewhen creating a new account; it does not store or display latest live balance. ctx7found FinTS docs for/nemiah/phpfints; relevant notes: FinTS/HBCI supports account access and balance retrieval, and real deployments need a registered product/application id.ctx7did 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
accountBalancestable inconvex/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
upsertAccountBalanceinternal mutation inconvex/bank/internal.ts. - Add
listLatestAccountBalancesinternal query that returns one latest balance per account for the authenticated user's accounts. - Keep
accounts.openingBalanceunchanged; 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
NormalizedBalancewith optionalasOf?: string. -
Keep comdirect
getBalance(accountExternalId)as the per-account lookup, but add a bulk path that maps/api/banking/clients/user/v2/accounts/balancesintoNormalizedBalance[]. -
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
refreshBalancesInternalinconvex/bank/orchestrator.ts, reusing provider preference and fallback logic fromrunSyncInternal. - When transaction sync already fetches balances, call
upsertAccountBalanceduringpersistSyncResults()so a full sync also updates UI balances. - Keep this as a Convex action/query API first; add an HTTP route in
convex/http.tsonly 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
AccountBalanceStripthat callsapi.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
RefreshCwicon fromlucide-react. -
Put the strip near the top of
DashboardPage, above the period-based KPI cards. This avoids confusing live bank balance with the existing filteredSaldoKPI. -
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
Saldolabel as period result, or rename it toPeriodensaldoto avoid ambiguity. - Show
Noch nicht abgerufenfor 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-testwithedge-runtime, matching existing project guidance. -
Test latest-balance query for
fresh,missing, anderrorstates. -
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
- Add schema and internal persistence.
- Add backend query tests and make them fail.
- Implement internal balance mutations/queries.
- Extract provider balance helpers.
- Add refresh action and wire provider fallback.
- Update transaction sync to persist balances opportunistically.
- Add UI component and Dashboard placement.
- Add Settings account-row balance display.
- Run
pnpm lint. - Run
pnpm build. - Run targeted Vitest suites.
Manual Verification
- Set provider preference to
fints, enter FinTS PIN if not in Convex env, refresh balances. - Confirm dashboard shows
Aktueller Kontostandper account. - Confirm existing
Saldo/Periodensaldostill follows the selected date range. - Set provider preference to
autowith broken comdirect REST credentials and valid FinTS config; confirm fallback works. - Run a normal transaction sync and confirm balances update without pressing the balance refresh button separately.