feat: add weekly financial digest with AI-powered summaries#607
feat: add weekly financial digest with AI-powered summaries#607GPradaT wants to merge 5 commits intorohitdash08:mainfrom
Conversation
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
There was a problem hiding this comment.
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
WeeklyDigestmodel +weekly_digeststable and unique(user_id, week_start)constraint. - Implements digest aggregation + summary generation (Gemini with heuristic fallback).
- Adds
/digest/latest,/digest/history, and/digest/generateendpoints 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.
| .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) |
There was a problem hiding this comment.
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).
| .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) |
| with url_request.urlopen(req, timeout=15) as resp: | ||
| payload = json.loads(resp.read().decode("utf-8")) |
There was a problem hiding this comment.
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.
| # 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() | ||
|
|
There was a problem hiding this comment.
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.
| """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, | ||
| ) |
There was a problem hiding this comment.
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.
| from ..models import WeeklyDigest | ||
| from ..services.digest import generate_digest | ||
|
|
||
| bp = Blueprint("digest", __name__, url_prefix="/digest") |
There was a problem hiding this comment.
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.
| bp = Blueprint("digest", __name__, url_prefix="/digest") | |
| bp = Blueprint("digest", __name__) |
| user_id = get_jwt_identity() | ||
| digest = ( |
There was a problem hiding this comment.
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.
| 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) |
There was a problem hiding this comment.
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.
…it nosec, query join
- 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
Demo VideoFull demo showing all three features working against a local dev server (SQLite + fakeredis):
Recorded on local Kubuntu dev environment, March 23 2026. |
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Implements the smart weekly financial digest feature (#121).
What it does:
New files:
services/digest.py— Core digest generation logicroutes/digest.py— REST API endpointstests/test_digest.py— 11 tests covering boundaries, data building, summaries, and endpointsEndpoints:
/digest/latest/digest/history?limit=N/digest/generateSchema:
Added
weekly_digeststable with unique constraint on (user_id, week_start).Test plan
Closes #121
/claim #121