Window dashboard Money Saved to 30-day average

This commit is contained in:
prosolis
2026-06-20 11:57:54 -07:00
parent 2fd4019103
commit ed03494edf
5 changed files with 55 additions and 4 deletions

View File

@@ -5,6 +5,7 @@ import (
"os"
"path/filepath"
"testing"
"time"
"veola/internal/crypto"
"veola/internal/models"
@@ -74,6 +75,51 @@ func TestDedupByItemAndURL(t *testing.T) {
}
}
func TestDashboardMoneySavedWindow(t *testing.T) {
if os.Getenv("CI_SKIP_SQLITE") != "" {
t.Skip()
}
s := newTestStore(t)
ctx := context.Background()
bp := 100.0
id, err := s.CreateItem(ctx, &models.Item{
Name: "Windowed", NtfyTopic: "veola", Active: true,
PollIntervalMinutes: 60, NtfyPriority: "default",
BestPrice: &bp,
})
if err != nil {
t.Fatal(err)
}
now := time.Now()
// A stale point well outside the 30-day window at a much higher price. If
// the average were computed over all history this would inflate "saved".
if err := s.InsertPricePoint(ctx, &models.PricePoint{
ItemID: id, Price: 500, PolledAt: now.AddDate(0, 0, -60),
}); err != nil {
t.Fatal(err)
}
// A recent point inside the window. Best price (100) is below it, so the
// only legitimate saving is 120 - 100 = 20.
if err := s.InsertPricePoint(ctx, &models.PricePoint{
ItemID: id, Price: 120, PolledAt: now.AddDate(0, 0, -5),
}); err != nil {
t.Fatal(err)
}
stats, err := s.GetDashboardStats(ctx)
if err != nil {
t.Fatal(err)
}
if stats.SavedItemCount != 1 {
t.Fatalf("expected 1 saved item, got %d", stats.SavedItemCount)
}
if got := stats.MoneySaved; got < 19.99 || got > 20.01 {
t.Errorf("expected money saved ~20 (recent avg only), got %.2f", got)
}
}
func TestCJKRoundTrip(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()

View File

@@ -1040,11 +1040,16 @@ func (s *Store) GetDashboardStats(ctx context.Context) (*DashboardStats, error)
`).Scan(&d.UnpricedCount); err != nil {
return nil, err
}
// "Money saved" compares each item's current best price against its recent
// market average. The average is windowed to the last 30 days so stale
// historical prices don't anchor the baseline; items whose only price
// history predates the window simply don't contribute an estimate.
rows, err := s.DB.QueryContext(ctx, `
SELECT i.best_price, AVG(p.price) AS avg_price
FROM items i
JOIN price_history p ON p.item_id = i.id
WHERE i.active = 1 AND i.best_price IS NOT NULL
AND p.polled_at >= datetime('now', '-30 day')
GROUP BY i.id
`)
if err != nil {

View File

@@ -137,7 +137,7 @@ func digestHTML(items []models.Item, stats *db.DashboardStats) string {
b.WriteString(`<div style="font-family:sans-serif">`)
b.WriteString(`<h2>Veola weekly digest</h2>`)
if stats != nil {
fmt.Fprintf(&b, `<p>%d active items. Estimated money saved so far: $%.2f.</p>`,
fmt.Fprintf(&b, `<p>%d active items. Estimated money saved vs 30-day average: $%.2f.</p>`,
stats.ActiveItems, stats.MoneySaved)
}
b.WriteString(`<table style="border-collapse:collapse" cellpadding="6">`)
@@ -158,7 +158,7 @@ func digestText(items []models.Item, stats *db.DashboardStats) string {
var b strings.Builder
b.WriteString("Veola weekly digest\n\n")
if stats != nil {
fmt.Fprintf(&b, "%d active items. Estimated money saved so far: $%.2f.\n\n", stats.ActiveItems, stats.MoneySaved)
fmt.Fprintf(&b, "%d active items. Estimated money saved vs 30-day average: $%.2f.\n\n", stats.ActiveItems, stats.MoneySaved)
}
for _, it := range items {
best := "no price yet"