Skip to content

feat: add weekly financial digest with AI-powered summaries#607

Open
GPradaT wants to merge 5 commits intorohitdash08:mainfrom
GPradaT:feat/weekly-digest
Open

feat: add weekly financial digest with AI-powered summaries#607
GPradaT wants to merge 5 commits intorohitdash08:mainfrom
GPradaT:feat/weekly-digest

Conversation

@GPradaT
Copy link

@GPradaT GPradaT commented Mar 22, 2026

Summary

Implements the smart weekly financial digest feature (#121).

What it does:

  • Generates weekly spending summaries with category breakdowns
  • Computes week-over-week spending trends (% change)
  • Provides actionable financial tips based on spending patterns
  • Supports AI-powered summaries via Gemini (falls back to heuristic when unavailable)
  • Stores digests with idempotent generation (one per user per week)

New files:

  • services/digest.py — Core digest generation logic
  • routes/digest.py — REST API endpoints
  • tests/test_digest.py — 11 tests covering boundaries, data building, summaries, and endpoints

Endpoints:

Method Path Description
GET /digest/latest Get most recent digest
GET /digest/history?limit=N Get digest history (max 52)
POST /digest/generate Trigger digest generation

Schema:
Added weekly_digests table with unique constraint on (user_id, week_start).

Test plan

  • Week boundary calculation tests (Mon-Sun)
  • Digest data aggregation with expenses
  • Heuristic summary generation (positive/negative flow)
  • API endpoint tests (empty state, generation, idempotency, history)
  • Integration test with Gemini API (requires API key)

Closes #121

/claim #121

Implements smart weekly digest generation (rohitdash08#121):
- WeeklyDigest model with unique constraint per user/week
- Digest service with heuristic and Gemini AI summary generation
- Week-over-week spending comparison and trend detection
- REST endpoints: GET /digest/latest, GET /digest/history, POST /digest/generate
- Comprehensive test suite (5 unit tests + 6 integration tests)
- Database schema migration for weekly_digests table

The digest analyzes expenses by category, computes week-over-week changes,
and generates actionable tips. Falls back to heuristic when AI is unavailable.

Closes rohitdash08#121
@GPradaT GPradaT requested a review from rohitdash08 as a code owner March 22, 2026 14:16
Copilot AI review requested due to automatic review settings March 22, 2026 14:16
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new “weekly financial digest” feature to the backend, including persistence, generation logic (with optional Gemini summaries), and REST endpoints to fetch/generate digests.

Changes:

  • Introduces WeeklyDigest model + weekly_digests table and unique (user_id, week_start) constraint.
  • Implements digest aggregation + summary generation (Gemini with heuristic fallback).
  • Adds /digest/latest, /digest/history, and /digest/generate endpoints with accompanying tests.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
packages/backend/app/services/digest.py Core digest generation, aggregation queries, heuristic + Gemini summary logic
packages/backend/app/routes/digest.py New authenticated endpoints for latest/history/generate digest
packages/backend/app/routes/init.py Registers the new digest blueprint
packages/backend/app/models.py Adds WeeklyDigest SQLAlchemy model + uniqueness constraint
packages/backend/app/db/schema.sql Adds weekly_digests table schema + uniqueness constraint
packages/backend/tests/test_digest.py Unit + endpoint tests for digest boundaries, aggregation, summaries, and API flow

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +39 to +46
.outerjoin(Category, Expense.category_id == Category.id)
.filter(
Expense.user_id == user_id,
Expense.spent_at >= start,
Expense.spent_at <= end,
Expense.expense_type != "INCOME",
)
.group_by(Category.name)
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _weekly_expenses query is joining Category while Category is already the lead entity in the query, which risks an incorrect/self-join and/or cartesian product. Consider querying from Expense (or select_from(Expense)), then outer-joining Category with a Category.user_id == user_id condition (to prevent cross-user category name leakage) and grouping by Expense.category_id + Category.name (similar to the dashboard category breakdown).

Suggested change
.outerjoin(Category, Expense.category_id == Category.id)
.filter(
Expense.user_id == user_id,
Expense.spent_at >= start,
Expense.spent_at <= end,
Expense.expense_type != "INCOME",
)
.group_by(Category.name)
.select_from(Expense)
.outerjoin(
Category,
(Expense.category_id == Category.id) & (Category.user_id == user_id),
)
.filter(
Expense.user_id == user_id,
Expense.spent_at >= start,
Expense.spent_at <= end,
Expense.expense_type != "INCOME",
)
.group_by(Expense.category_id, Category.name)

Copilot uses AI. Check for mistakes.
Comment on lines +171 to +172
with url_request.urlopen(req, timeout=15) as resp:
payload = json.loads(resp.read().decode("utf-8"))
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bandit will flag urllib.request.urlopen usage (B310) in CI. app/services/ai.py suppresses this with # nosec B310; this new call site should either use the existing requests dependency (preferred for readability/testing) or add an explicit # nosec B310 with justification to avoid failing the security scan.

Copilot uses AI. Check for mistakes.
Comment on lines +195 to +232
# Check if digest already exists for this week
existing = WeeklyDigest.query.filter_by(
user_id=user_id, week_start=start
).first()
if existing:
return existing

data = _build_digest_data(user_id, start, end)

# Skip if no transactions
if data["transaction_count"] == 0:
return None

# Try AI, fall back to heuristic
api_key = (_settings.gemini_api_key or "").strip()
model = _settings.gemini_model
if api_key:
try:
result = _ai_summary(data, api_key, model)
except Exception:
logger.warning("Gemini digest failed, falling back to heuristic", exc_info=True)
result = _heuristic_summary(data)
else:
result = _heuristic_summary(data)

digest = WeeklyDigest(
user_id=user_id,
week_start=start,
week_end=end,
summary=result.get("summary", ""),
tips=json.dumps(result.get("tips", [])),
highlights=json.dumps(result.get("highlights", [])),
raw_data=json.dumps(data),
method=result.get("method", "unknown"),
)
db.session.add(digest)
db.session.commit()

Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generate_digest relies on a unique constraint for idempotency but isn’t race-safe: two concurrent calls can both miss existing and then one will fail on commit with an IntegrityError. Catch the integrity error, rollback, and re-fetch the existing digest (or use an upsert) so concurrent calls don’t 500.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +15
"""Tests for weekly digest feature."""

import json
from datetime import date, timedelta

import pytest

from app.extensions import db
from app.models import Expense, Category, WeeklyDigest
from app.services.digest import (
generate_digest,
_week_boundaries,
_build_digest_data,
_heuristic_summary,
)
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test module has several unused imports (json, WeeklyDigest, generate_digest), which will fail flake8 in CI (F401). Remove unused imports or use them in assertions.

Copilot uses AI. Check for mistakes.
from ..models import WeeklyDigest
from ..services.digest import generate_digest

bp = Blueprint("digest", __name__, url_prefix="/digest")
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bp is created with url_prefix='/digest' but register_routes() also registers this blueprint with url_prefix='/digest', which will mount routes under /digest/digest/... and break the intended endpoints (and the tests in this PR). Remove the url_prefix from the Blueprint(...) definition (to match other route modules) or stop passing url_prefix when registering it.

Suggested change
bp = Blueprint("digest", __name__, url_prefix="/digest")
bp = Blueprint("digest", __name__)

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +18
user_id = get_jwt_identity()
digest = (
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_jwt_identity() returns a string user id (see auth.py creating tokens with identity=str(user.id)). Other routes cast it to int, but this file uses it as-is, which can cause type mismatches in queries (and inconsistent behavior across DBs). Cast to int in all digest endpoints before using it in filters/service calls.

Copilot uses AI. Check for mistakes.
Comment on lines +33 to +39
limit = request.args.get("limit", 10, type=int)
limit = min(limit, 52) # cap at 1 year of weekly digests

digests = (
WeeklyDigest.query.filter_by(user_id=user_id)
.order_by(WeeklyDigest.week_start.desc())
.limit(limit)
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

limit = min(limit, 52) still allows limit to be 0 or negative (e.g. ?limit=-1), which can produce unexpected results or errors depending on the SQLAlchemy backend. Clamp to a sensible minimum (e.g. max(1, min(limit, 52))) or return 400 for non-positive limits.

Copilot uses AI. Check for mistakes.
GPradaT added 3 commits March 22, 2026 16:38
- Frontend: Digest page with latest summary, tips, highlights, and history
- Frontend: Generate button with loading spinner
- Frontend: API client (digest.ts) following existing pattern
- Frontend: Route in App.tsx + navigation link in Navbar
- OpenAPI: /digest/latest, /digest/history, /digest/generate paths
- OpenAPI: WeeklyDigest schema
@GPradaT
Copy link
Author

GPradaT commented Mar 23, 2026

Demo Video

Full demo showing all three features working against a local dev server (SQLite + fakeredis):

  • Multi-Account Dashboard: Create accounts, net worth calculation, deactivation
  • Savings Goals: Create goal, contributions, progress tracking with milestones (75%)
  • Weekly Digest: Expense creation, digest generation, history

Download demo video

Recorded on local Kubuntu dev environment, March 23 2026.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Smart digest with weekly financial summary

2 participants