Private
Public Access
1
0

Add 7-day sync overlap buffer and improve empty investments state

SimpleFIN sync now looks back 7 days from last sync to catch
retroactively posted transactions. Investments page shows account
balance instead of $0 summary cards when no holdings exist.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Catherine Renelle
2026-02-15 23:16:28 -05:00
parent e9ad7c0248
commit 24839a3a21
2 changed files with 106 additions and 85 deletions
@@ -29,90 +29,103 @@
<v-progress-linear v-if="loading" indeterminate color="primary" class="mb-4" /> <v-progress-linear v-if="loading" indeterminate color="primary" class="mb-4" />
<template v-if="performance && !loading"> <template v-if="performance && !loading">
<!-- Portfolio Summary --> <!-- No holdings: show account balance + prompt -->
<v-row class="mb-4"> <template v-if="performance.holdings.length === 0">
<v-col cols="12" sm="6" md="3"> <v-card class="text-center pa-8">
<v-card variant="tonal"> <div class="text-h4 mb-2" :class="selectedAccountBalance >= 0 ? 'text-success' : 'text-error'">
<v-card-text class="text-center"> {{ formatCurrency(selectedAccountBalance) }}
<div class="text-caption">Market Value</div> </div>
<div class="text-h6">{{ formatCurrency(performance.totalMarketValue) }}</div> <div class="text-caption text-medium-emphasis mb-6">Account Balance</div>
</v-card-text> <v-icon size="48" color="grey-lighten-1" class="mb-4">mdi-chart-pie</v-icon>
</v-card> <p class="text-medium-emphasis">No individual holdings tracked for this account.</p>
</v-col> <p class="text-medium-emphasis text-caption">Add holdings to see allocation, cost basis, and performance.</p>
<v-col cols="12" sm="6" md="3"> </v-card>
<v-card variant="tonal"> </template>
<v-card-text class="text-center">
<div class="text-caption">Cost Basis</div>
<div class="text-h6">{{ formatCurrency(performance.totalCostBasis) }}</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" sm="6" md="3">
<v-card variant="tonal" :color="performance.totalGainLoss >= 0 ? 'success' : 'error'">
<v-card-text class="text-center">
<div class="text-caption">Total Gain/Loss</div>
<div class="text-h6">{{ formatCurrency(performance.totalGainLoss) }}</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" sm="6" md="3">
<v-card variant="tonal" :color="performance.totalGainLossPercent >= 0 ? 'success' : 'error'">
<v-card-text class="text-center">
<div class="text-caption">Return</div>
<div class="text-h6">{{ formatPercent(performance.totalGainLossPercent) }}</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- Allocation Chart + Holdings Table --> <!-- Has holdings: show full portfolio view -->
<v-row> <template v-else>
<v-col v-if="performance.holdings.length > 0" cols="12" md="4"> <!-- Portfolio Summary -->
<v-card> <v-row class="mb-4">
<v-card-title>Allocation</v-card-title> <v-col cols="12" sm="6" md="3">
<v-card-text> <v-card variant="tonal">
<apexchart <v-card-text class="text-center">
type="donut" <div class="text-caption">Market Value</div>
height="300" <div class="text-h6">{{ formatCurrency(performance.totalMarketValue) }}</div>
:options="allocationChartOptions" </v-card-text>
:series="allocationChartSeries" </v-card>
/> </v-col>
</v-card-text> <v-col cols="12" sm="6" md="3">
</v-card> <v-card variant="tonal">
</v-col> <v-card-text class="text-center">
<v-col cols="12" :md="performance.holdings.length > 0 ? 8 : 12"> <div class="text-caption">Cost Basis</div>
<v-card> <div class="text-h6">{{ formatCurrency(performance.totalCostBasis) }}</div>
<v-card-title>Holdings</v-card-title> </v-card-text>
<v-card-text v-if="performance.holdings.length > 0"> </v-card>
<v-data-table </v-col>
:headers="holdingsHeaders" <v-col cols="12" sm="6" md="3">
:items="performance.holdings" <v-card variant="tonal" :color="performance.totalGainLoss >= 0 ? 'success' : 'error'">
density="compact" <v-card-text class="text-center">
:items-per-page="-1" <div class="text-caption">Total Gain/Loss</div>
hide-default-footer <div class="text-h6">{{ formatCurrency(performance.totalGainLoss) }}</div>
> </v-card-text>
<template #item.shares="{ item }">{{ item.shares.toFixed(4) }}</template> </v-card>
<template #item.costBasis="{ item }">{{ formatCurrency(item.costBasis) }}</template> </v-col>
<template #item.currentPrice="{ item }">{{ formatCurrency(item.currentPrice) }}</template> <v-col cols="12" sm="6" md="3">
<template #item.marketValue="{ item }">{{ formatCurrency(item.marketValue) }}</template> <v-card variant="tonal" :color="performance.totalGainLossPercent >= 0 ? 'success' : 'error'">
<template #item.gainLoss="{ item }"> <v-card-text class="text-center">
<span :class="item.gainLoss >= 0 ? 'text-success' : 'text-error'"> <div class="text-caption">Return</div>
{{ formatCurrency(item.gainLoss) }} <div class="text-h6">{{ formatPercent(performance.totalGainLossPercent) }}</div>
</span> </v-card-text>
</template> </v-card>
<template #item.gainLossPercent="{ item }"> </v-col>
<span :class="item.gainLossPercent >= 0 ? 'text-success' : 'text-error'"> </v-row>
{{ formatPercent(item.gainLossPercent) }}
</span> <!-- Allocation Chart + Holdings Table -->
</template> <v-row>
</v-data-table> <v-col cols="12" md="4">
</v-card-text> <v-card>
<v-card-text v-else class="text-center pa-8 text-medium-emphasis"> <v-card-title>Allocation</v-card-title>
No holdings found for this account. Add investment holdings to track your portfolio. <v-card-text>
</v-card-text> <apexchart
</v-card> type="donut"
</v-col> height="300"
</v-row> :options="allocationChartOptions"
:series="allocationChartSeries"
/>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="8">
<v-card>
<v-card-title>Holdings</v-card-title>
<v-card-text>
<v-data-table
:headers="holdingsHeaders"
:items="performance.holdings"
density="compact"
:items-per-page="-1"
hide-default-footer
>
<template #item.shares="{ item }">{{ item.shares.toFixed(4) }}</template>
<template #item.costBasis="{ item }">{{ formatCurrency(item.costBasis) }}</template>
<template #item.currentPrice="{ item }">{{ formatCurrency(item.currentPrice) }}</template>
<template #item.marketValue="{ item }">{{ formatCurrency(item.marketValue) }}</template>
<template #item.gainLoss="{ item }">
<span :class="item.gainLoss >= 0 ? 'text-success' : 'text-error'">
{{ formatCurrency(item.gainLoss) }}
</span>
</template>
<template #item.gainLossPercent="{ item }">
<span :class="item.gainLossPercent >= 0 ? 'text-success' : 'text-error'">
{{ formatPercent(item.gainLossPercent) }}
</span>
</template>
</v-data-table>
</v-card-text>
</v-card>
</v-col>
</v-row>
</template>
</template> </template>
<v-card v-if="!loading && !performance && selectedAccountId" class="text-center pa-8"> <v-card v-if="!loading && !performance && selectedAccountId" class="text-center pa-8">
@@ -158,6 +171,11 @@ const investmentAccounts = computed(() =>
})) }))
) )
const selectedAccountBalance = computed(() => {
const acct = investmentAccounts.value.find(a => a.id === selectedAccountId.value)
return acct?.balance ?? 0
})
// --- Allocation Chart --- // --- Allocation Chart ---
const allocationChartSeries = computed(() => const allocationChartSeries = computed(() =>
performance.value?.holdings.map(h => h.marketValue) ?? [] performance.value?.holdings.map(h => h.marketValue) ?? []
+5 -2
View File
@@ -424,12 +424,15 @@ public class BankSyncService : IBankSyncService
// For SimpleFIN, use the earliest start date across all accounts so the // For SimpleFIN, use the earliest start date across all accounts so the
// single API call (cached) returns transactions covering every account. // single API call (cached) returns transactions covering every account.
// Subtract a 7-day overlap buffer because banks may post transactions with
// dates earlier than when they appear in the SimpleFIN feed. Duplicate
// detection (FitId + fingerprint) ensures re-fetched transactions are skipped.
var endDate = DateTime.UtcNow; var endDate = DateTime.UtcNow;
DateTime? simpleFinStartDate = null; DateTime? simpleFinStartDate = null;
if (connection.Provider == SyncProvider.SimpleFIN) if (connection.Provider == SyncProvider.SimpleFIN)
{ {
simpleFinStartDate = enabledAccounts simpleFinStartDate = enabledAccounts
.Select(la => la.LastSyncAt ?? endDate.AddDays(-90)) .Select(la => la.LastSyncAt?.AddDays(-7) ?? endDate.AddDays(-90))
.Min(); .Min();
} }
@@ -447,7 +450,7 @@ public class BankSyncService : IBankSyncService
try try
{ {
var startDate = simpleFinStartDate ?? linkedAccount.LastSyncAt ?? endDate.AddDays(-90); var startDate = simpleFinStartDate ?? linkedAccount.LastSyncAt?.AddDays(-7) ?? endDate.AddDays(-90);
var provider = connection.Provider == SyncProvider.Plaid var provider = connection.Provider == SyncProvider.Plaid
? (Purrse.Plugins.Abstractions.IAccountSyncProvider)_plaid ? (Purrse.Plugins.Abstractions.IAccountSyncProvider)_plaid