Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,41 @@ OpenAPI: `backend/app/openapi.yaml`
- Bills: CRUD `/bills`, pay/mark `/bills/{id}/pay`
- Reminders: CRUD `/reminders`, trigger `/reminders/run`
- Insights: `/insights/monthly`, `/insights/budget-suggestion`
- Digest: `/digest/weekly`, `/digest/weekly/send`, `/digest/weekly/preview`

## Weekly Financial Digest

FinMind provides a **Smart Weekly Financial Summary** that delivers actionable insights directly to users' inboxes.

### Features
- **Comprehensive Overview**: Total income, expenses, net flow, and savings rate for the week
- **Week-over-Week Trends**: Compare spending and income changes from the previous week
- **Category Breakdown**: Detailed spending analysis by category with percentages
- **Notable Transactions**: Highlights of significant expenses and income
- **Upcoming Bills**: Preview of bills due in the next 7 days
- **Smart Insights**: AI-generated financial tips and recommendations

### Endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/digest/weekly` | GET | Get weekly digest data (JSON) |
| `/digest/weekly/send` | POST | Send digest email to current user |
| `/digest/weekly/preview` | GET | Preview email subject and body |

### Scheduled Delivery
The weekly digest is automatically generated and sent to all users every **Sunday at 9:00 AM UTC** via APScheduler.

### Manual Trigger (CLI)
```bash
# Generate digest for all users
flask generate-digest

# Generate for specific user with email
flask generate-digest --user-id 1 --send-email
```

### Scheduler Status
Check scheduler status at `/scheduler/status` endpoint.

## MVP UI/UX Plan
- Auth screens: register/login.
Expand Down Expand Up @@ -104,11 +139,14 @@ finmind/
bills.py
reminders.py
insights.py
digest.py
services/
__init__.py
ai.py
cache.py
reminders.py
digest.py
scheduler.py
db/
schema.sql
openapi.yaml
Expand Down
41 changes: 41 additions & 0 deletions packages/backend/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@
import logging
from datetime import timedelta

# Scheduler imports
from .services.scheduler import (
init_scheduler,
start_scheduler,
shutdown_scheduler,
get_scheduler_status,
)


def create_app(settings: Settings | None = None) -> Flask:
app = Flask(__name__)
Expand Down Expand Up @@ -56,6 +64,12 @@ def create_app(settings: Settings | None = None) -> Flask:
with app.app_context():
_ensure_schema_compatibility(app)

# Initialize scheduler (but don't start automatically in testing)
if not app.config.get("TESTING"):
init_scheduler(app)
start_scheduler()
logger.info("Scheduler initialized and started")

@app.before_request
def _before_request():
init_request_context()
Expand All @@ -73,6 +87,11 @@ def metrics():
obs = app.extensions["observability"]
return obs.metrics_response()

@app.get("/scheduler/status")
def scheduler_status():
"""Get the current status of scheduled jobs."""
return jsonify(get_scheduler_status()), 200

@app.errorhandler(500)
def internal_error(_error):
return jsonify(error="internal server error"), 500
Expand All @@ -93,6 +112,28 @@ def init_db():
finally:
conn.close()

@app.cli.command("generate-digest")
@click.option("--user-id", type=int, default=None, help="Generate for specific user")
@click.option("--send-email", is_flag=True, help="Send email to user")
def generate_digest(user_id, send_email):
"""Generate weekly digest for testing."""
from .services.digest import WeeklyDigestService

with app.app_context():
if user_id:
week_start, week_end = WeeklyDigestService.get_week_bounds()
summary = WeeklyDigestService.generate_weekly_summary(
user_id, week_start, week_end
)
if send_email:
success = WeeklyDigestService.send_digest_email(user_id, summary)
click.echo(f"Digest sent: {success}")
else:
click.echo(f"Digest generated: {summary}")
else:
results = WeeklyDigestService.generate_and_send_all_digests()
click.echo(f"Batch results: {results}")

return app


Expand Down
2 changes: 2 additions & 0 deletions packages/backend/app/routes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from .categories import bp as categories_bp
from .docs import bp as docs_bp
from .dashboard import bp as dashboard_bp
from .digest import bp as digest_bp


def register_routes(app: Flask):
Expand All @@ -18,3 +19,4 @@ def register_routes(app: Flask):
app.register_blueprint(categories_bp, url_prefix="/categories")
app.register_blueprint(docs_bp, url_prefix="/docs")
app.register_blueprint(dashboard_bp, url_prefix="/dashboard")
app.register_blueprint(digest_bp, url_prefix="/digest")
148 changes: 148 additions & 0 deletions packages/backend/app/routes/digest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
"""
Weekly Digest API Routes

Endpoints for generating and retrieving weekly financial summaries.
"""

from datetime import date, datetime, timedelta
from flask import Blueprint, jsonify, request
from flask_jwt_extended import jwt_required, get_jwt_identity

from ..services.digest import WeeklyDigestService
from ..models import User
from ..extensions import db
import logging

bp = Blueprint("digest", __name__)
logger = logging.getLogger("finmind.digest")


@bp.get("/weekly")
@jwt_required()
def get_weekly_digest():
"""
Get the weekly financial digest for the current user.

Query params:
week_start: Optional ISO date string (YYYY-MM-DD) for the start of the week.
Defaults to the current week's Monday.

Returns:
JSON object containing:
- period: Week start/end dates
- summary: Income, expenses, net flow, savings rate
- trends: Week-over-week changes
- spending_by_category: Category breakdown with percentages
- notable_transactions: Top transactions by amount
- upcoming_bills: Bills due in the next 7 days
- insights: Actionable financial insights
"""
uid = int(get_jwt_identity())

# Parse optional week_start parameter
week_start_param = request.args.get("week_start", "").strip()

try:
if week_start_param:
week_start = date.fromisoformat(week_start_param)
# Ensure week_start is a Monday
days_since_monday = week_start.weekday()
if days_since_monday != 0:
week_start = week_start - timedelta(days=days_since_monday)
week_end = week_start + timedelta(days=6)
else:
week_start, week_end = WeeklyDigestService.get_week_bounds()
except ValueError:
return jsonify(error="Invalid week_start format. Use YYYY-MM-DD."), 400

try:
summary = WeeklyDigestService.generate_weekly_summary(
uid, week_start, week_end
)
logger.info("Weekly digest generated for user %s", uid)
return jsonify(summary)

except Exception as e:
logger.error("Error generating weekly digest: %s", str(e))
return jsonify(error="Failed to generate weekly digest"), 500


@bp.post("/weekly/send")
@jwt_required()
def send_weekly_digest():
"""
Send the weekly digest email to the current user.

Returns:
JSON object with success status and message.
"""
uid = int(get_jwt_identity())

try:
user = db.session.get(User, uid)
if not user:
return jsonify(error="User not found"), 404

week_start, week_end = WeeklyDigestService.get_week_bounds()
summary = WeeklyDigestService.generate_weekly_summary(
uid, week_start, week_end
)

if summary["summary"]["transaction_count"] == 0:
return jsonify(
success=False,
message="No transactions this week. Email not sent."
), 200

success = WeeklyDigestService.send_digest_email(uid, summary)

if success:
logger.info("Weekly digest email sent to user %s", uid)
return jsonify(
success=True,
message="Weekly digest email sent successfully."
), 200
else:
return jsonify(
success=False,
message="Failed to send email. Please check your email settings."
), 500

except Exception as e:
logger.error("Error sending weekly digest: %s", str(e))
return jsonify(error="Failed to send weekly digest"), 500


@bp.get("/weekly/preview")
@jwt_required()
def preview_weekly_digest():
"""
Preview the weekly digest email content.

Returns:
JSON object with subject and body of the email.
"""
uid = int(get_jwt_identity())

try:
user = db.session.get(User, uid)
if not user:
return jsonify(error="User not found"), 404

week_start, week_end = WeeklyDigestService.get_week_bounds()
summary = WeeklyDigestService.generate_weekly_summary(
uid, week_start, week_end
)

subject, body = WeeklyDigestService.format_digest_email(summary, user)

return jsonify(
subject=subject,
body=body,
summary=summary,
), 200

except Exception as e:
logger.error("Error previewing weekly digest: %s", str(e))
return jsonify(error="Failed to preview weekly digest"), 500

Loading