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
45 changes: 45 additions & 0 deletions DEMO_INSTRUCTIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Com gravar el video demo pels PRs de FinMind

## Que hem fet (resum)

Hem creat 3 features pel backend de FinMind (Python/Flask):

### PR #609 — Multi-Account Dashboard ($200)
- Crear comptes financers (checking, savings, credit card)
- Veure resum amb net worth (assets - deutes)
- Desactivar comptes (soft-delete)

### PR #608 — Savings Goals ($250)
- Crear objectius d'estalvi amb target i deadline
- Afegir contribucions
- Veure progrés amb milestones (25/50/75/100%)
- Auto-completa quan arribes al target

### PR #607 — Weekly Digest ($500)
- Genera resum setmanal de despeses
- Compara amb la setmana anterior (week-over-week %)
- Tips automàtics basats en patrons de despesa

## Com gravar

### 1. Obre 2 terminals

### 2. Terminal 1 — Arrenca el servidor:
```bash
cd /home/clawd/workspace/bounties/finmind/packages/backend
bash start_demo_server.sh
```
Espera a veure "Running on http://127.0.0.1:5556"

### 3. Terminal 2 — Comença a gravar pantalla, llavors executa:
```bash
cd /home/clawd/workspace/bounties/finmind
bash run_demo.sh
```

### 4. Para la gravació

### 5. Puja el video com a comentari als 3 PRs:
- https://github.com/rohitdash08/FinMind/pull/607
- https://github.com/rohitdash08/FinMind/pull/608
- https://github.com/rohitdash08/FinMind/pull/609
9 changes: 9 additions & 0 deletions app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import NotFound from "./pages/NotFound";
import { Landing } from "./pages/Landing";
import ProtectedRoute from "./components/auth/ProtectedRoute";
import Account from "./pages/Account";
import Digest from "./pages/Digest";

const queryClient = new QueryClient({
defaultOptions: {
Expand Down Expand Up @@ -83,6 +84,14 @@ const App = () => (
</ProtectedRoute>
}
/>
<Route
path="digest"
element={
<ProtectedRoute>
<Digest />
</ProtectedRoute>
}
/>
<Route
path="account"
element={
Expand Down
24 changes: 24 additions & 0 deletions app/src/api/digest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { api } from './client';

export type WeeklyDigest = {
id: number;
week_start: string;
week_end: string;
summary: string;
tips: string[];
highlights: string[];
method: string;
created_at: string;
};

export async function getLatestDigest(): Promise<WeeklyDigest> {
return api<WeeklyDigest>('/digest/latest');
}

export async function getDigestHistory(limit = 10): Promise<WeeklyDigest[]> {
return api<WeeklyDigest[]>(`/digest/history?limit=${limit}`);
}

export async function generateDigest(): Promise<WeeklyDigest> {
return api<WeeklyDigest>('/digest/generate', { method: 'POST' });
}
1 change: 1 addition & 0 deletions app/src/components/layout/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const navigation = [
{ name: 'Reminders', href: '/reminders' },
{ name: 'Expenses', href: '/expenses' },
{ name: 'Analytics', href: '/analytics' },
{ name: 'Digest', href: '/digest' },
];

export function Navbar() {
Expand Down
143 changes: 143 additions & 0 deletions app/src/pages/Digest.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { useState, useEffect, useCallback } from 'react';
import { FinancialCard, FinancialCardContent, FinancialCardHeader, FinancialCardTitle } from '@/components/ui/financial-card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { FileText, RefreshCw, Lightbulb, TrendingUp, Calendar } from 'lucide-react';
import { useToast } from '@/hooks/use-toast';
import { getLatestDigest, getDigestHistory, generateDigest, type WeeklyDigest } from '@/api/digest';

export default function Digest() {
const { toast } = useToast();
const [latest, setLatest] = useState<WeeklyDigest | null>(null);
const [history, setHistory] = useState<WeeklyDigest[]>([]);
const [loading, setLoading] = useState(true);
const [generating, setGenerating] = useState(false);

const loadData = useCallback(async () => {
try {
const [lat, hist] = await Promise.allSettled([getLatestDigest(), getDigestHistory()]);
if (lat.status === 'fulfilled') setLatest(lat.value);
if (hist.status === 'fulfilled') setHistory(hist.value);
} catch {
// No digest yet is fine
} finally { setLoading(false); }
}, []);

useEffect(() => { loadData(); }, [loadData]);

const handleGenerate = async () => {
setGenerating(true);
try {
const d = await generateDigest();
setLatest(d);
toast({ title: 'Digest generated!' });
loadData();
} catch (e: unknown) {
toast({ title: 'Error', description: e instanceof Error ? e.message : 'No transactions for last week', variant: 'destructive' });
} finally { setGenerating(false); }
};

if (loading) return <div className="flex justify-center p-8">Loading...</div>;

return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">Weekly Digest</h1>
<p className="text-muted-foreground">Your weekly financial summary and insights</p>
</div>
<Button onClick={handleGenerate} disabled={generating}>
<RefreshCw className={`mr-2 h-4 w-4 ${generating ? 'animate-spin' : ''}`} />
{generating ? 'Generating...' : 'Generate Digest'}
</Button>
</div>

{latest ? (
<FinancialCard>
<FinancialCardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<FileText className="h-5 w-5" />
<FinancialCardTitle>
Week of {latest.week_start} to {latest.week_end}
</FinancialCardTitle>
</div>
<Badge variant="outline">{latest.method}</Badge>
</div>
</FinancialCardHeader>
<FinancialCardContent className="space-y-4">
{/* Summary */}
<div className="whitespace-pre-line text-sm">{latest.summary}</div>

{/* Tips */}
{latest.tips.length > 0 && (
<div className="space-y-2">
<h3 className="text-sm font-semibold flex items-center gap-1">
<Lightbulb className="h-4 w-4 text-yellow-500" /> Tips
</h3>
<ul className="space-y-1">
{latest.tips.map((tip, i) => (
<li key={i} className="text-sm text-muted-foreground flex items-start gap-2">
<span className="text-yellow-500">•</span> {tip}
</li>
))}
</ul>
</div>
)}

{/* Highlights */}
{latest.highlights.length > 0 && (
<div className="space-y-2">
<h3 className="text-sm font-semibold flex items-center gap-1">
<TrendingUp className="h-4 w-4 text-green-500" /> Highlights
</h3>
<ul className="space-y-1">
{latest.highlights.map((h, i) => (
<li key={i} className="text-sm text-muted-foreground flex items-start gap-2">
<span className="text-green-500">•</span> {h}
</li>
))}
</ul>
</div>
)}
</FinancialCardContent>
</FinancialCard>
) : (
<FinancialCard>
<FinancialCardContent className="flex flex-col items-center justify-center py-12">
<FileText className="h-12 w-12 text-muted-foreground mb-4" />
<p className="text-muted-foreground mb-4">No digest yet. Add some expenses and generate your first weekly digest!</p>
<Button onClick={handleGenerate} disabled={generating}>
<RefreshCw className={`mr-2 h-4 w-4 ${generating ? 'animate-spin' : ''}`} />
Generate Now
</Button>
</FinancialCardContent>
</FinancialCard>
)}

{/* History */}
{history.length > 1 && (
<div className="space-y-3">
<h2 className="text-lg font-semibold flex items-center gap-2">
<Calendar className="h-5 w-5" /> Previous Digests
</h2>
{history.slice(1).map(d => (
<FinancialCard key={d.id}>
<FinancialCardHeader className="pb-2">
<div className="flex items-center justify-between">
<FinancialCardTitle className="text-sm">
{d.week_start} — {d.week_end}
</FinancialCardTitle>
<Badge variant="outline" className="text-xs">{d.method}</Badge>
</div>
</FinancialCardHeader>
<FinancialCardContent>
<p className="text-sm text-muted-foreground line-clamp-3">{d.summary}</p>
</FinancialCardContent>
</FinancialCard>
))}
</div>
)}
</div>
);
}
106 changes: 106 additions & 0 deletions demo.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#!/bin/bash
# FinMind Feature Demo - PRs #607, #608, #609
BASE=http://127.0.0.1:5555

echo "======================================"
echo " FinMind Feature Demo"
echo "======================================"
echo ""

# Register and login
echo ">>> Register user"
curl -s -X POST $BASE/auth/register -H "Content-Type: application/json" \
-d '{"email":"demo@test.com","password":"demo1234"}' | python3 -m json.tool
echo ""

echo ">>> Login"
TOKEN=$(curl -s -X POST $BASE/auth/login -H "Content-Type: application/json" \
-d '{"email":"demo@test.com","password":"demo1234"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
echo "Got token: ${TOKEN:0:20}..."
AUTH="Authorization: Bearer $TOKEN"
echo ""

# Add some expenses for digest
echo "======================================"
echo " PR #607: Weekly Digest"
echo "======================================"
echo ""

echo ">>> Adding expenses for last week..."
for i in 1 2 3 4 5; do
DAY=$(date -d "last monday + ${i} days" +%Y-%m-%d 2>/dev/null || date -v-7d +%Y-%m-%d)
curl -s -X POST $BASE/expenses -H "$AUTH" -H "Content-Type: application/json" \
-d "{\"amount\":$((50 + i * 15)),\"notes\":\"Expense day $i\",\"spent_at\":\"$DAY\"}" > /dev/null
done
echo "Added 5 expenses"
echo ""

echo ">>> POST /digest/generate"
curl -s -X POST $BASE/digest/generate -H "$AUTH" | python3 -m json.tool
echo ""

echo ">>> GET /digest/latest"
curl -s $BASE/digest/latest -H "$AUTH" | python3 -m json.tool
echo ""

echo ">>> GET /digest/history"
curl -s "$BASE/digest/history?limit=5" -H "$AUTH" | python3 -m json.tool
echo ""

# Savings Goals
echo "======================================"
echo " PR #608: Savings Goals"
echo "======================================"
echo ""

echo ">>> POST /goals/ (create goal)"
curl -s -X POST $BASE/goals/ -H "$AUTH" -H "Content-Type: application/json" \
-d '{"name":"Emergency Fund","target_amount":10000,"currency":"EUR","deadline":"2026-12-31"}' | python3 -m json.tool
echo ""

echo ">>> POST /goals/1/contribute (add 2500)"
curl -s -X POST $BASE/goals/1/contribute -H "$AUTH" -H "Content-Type: application/json" \
-d '{"amount":2500,"note":"First deposit"}' | python3 -m json.tool
echo ""

echo ">>> POST /goals/1/contribute (add 5000)"
curl -s -X POST $BASE/goals/1/contribute -H "$AUTH" -H "Content-Type: application/json" \
-d '{"amount":5000,"note":"Bonus savings"}' | python3 -m json.tool
echo ""

echo ">>> GET /goals/1/progress (75% with milestones)"
curl -s $BASE/goals/1/progress -H "$AUTH" | python3 -m json.tool
echo ""

echo ">>> GET /goals/1/contributions"
curl -s $BASE/goals/1/contributions -H "$AUTH" | python3 -m json.tool
echo ""

# Multi-account
echo "======================================"
echo " PR #609: Multi-Account Dashboard"
echo "======================================"
echo ""

echo ">>> POST /accounts/ (checking)"
curl -s -X POST $BASE/accounts/ -H "$AUTH" -H "Content-Type: application/json" \
-d '{"name":"Main Checking","account_type":"checking","balance":5000,"currency":"EUR"}' | python3 -m json.tool
echo ""

echo ">>> POST /accounts/ (savings)"
curl -s -X POST $BASE/accounts/ -H "$AUTH" -H "Content-Type: application/json" \
-d '{"name":"Savings","account_type":"savings","balance":15000,"currency":"EUR"}' | python3 -m json.tool
echo ""

echo ">>> POST /accounts/ (credit card)"
curl -s -X POST $BASE/accounts/ -H "$AUTH" -H "Content-Type: application/json" \
-d '{"name":"Credit Card","account_type":"credit","balance":2000,"currency":"EUR"}' | python3 -m json.tool
echo ""

echo ">>> GET /accounts/overview (net worth)"
curl -s $BASE/accounts/overview -H "$AUTH" | python3 -m json.tool
echo ""

echo "======================================"
echo " Demo Complete!"
echo "======================================"
14 changes: 14 additions & 0 deletions packages/backend/app/db/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,20 @@ CREATE TABLE IF NOT EXISTS user_subscriptions (
started_at TIMESTAMP NOT NULL DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS weekly_digests (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
week_start DATE NOT NULL,
week_end DATE NOT NULL,
summary TEXT NOT NULL,
tips TEXT NOT NULL DEFAULT '[]',
highlights TEXT NOT NULL DEFAULT '[]',
raw_data TEXT NOT NULL DEFAULT '{}',
method VARCHAR(20) NOT NULL DEFAULT 'heuristic',
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE (user_id, week_start)
);

CREATE TABLE IF NOT EXISTS audit_logs (
id SERIAL PRIMARY KEY,
user_id INT REFERENCES users(id) ON DELETE SET NULL,
Expand Down
18 changes: 18 additions & 0 deletions packages/backend/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,24 @@ class UserSubscription(db.Model):
started_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)


class WeeklyDigest(db.Model):
__tablename__ = "weekly_digests"
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
week_start = db.Column(db.Date, nullable=False)
week_end = db.Column(db.Date, nullable=False)
summary = db.Column(db.Text, nullable=False)
tips = db.Column(db.Text, default="[]", nullable=False)
highlights = db.Column(db.Text, default="[]", nullable=False)
raw_data = db.Column(db.Text, default="{}", nullable=False)
method = db.Column(db.String(20), default="heuristic", nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)

__table_args__ = (
db.UniqueConstraint("user_id", "week_start", name="uq_user_week"),
)


class AuditLog(db.Model):
__tablename__ = "audit_logs"
id = db.Column(db.Integer, primary_key=True)
Expand Down
Loading