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
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 { WeeklyDigest } from "./pages/WeeklyDigest";

const queryClient = new QueryClient({
defaultOptions: {
Expand Down Expand Up @@ -83,6 +84,14 @@ const App = () => (
</ProtectedRoute>
}
/>
<Route
path="weekly-digest"
element={
<ProtectedRoute>
<WeeklyDigest />
</ProtectedRoute>
}
/>
<Route
path="account"
element={
Expand Down
129 changes: 129 additions & 0 deletions app/src/__tests__/WeeklyDigest.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { render, screen, waitFor } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { BrowserRouter } from 'react-router-dom';
import { WeeklyDigest } from '../pages/WeeklyDigest';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

// Mock the API
vi.mock('@/api/weeklyDigest', () => ({
getWeeklySummary: vi.fn().mockResolvedValue({
week_start: '2024-01-01',
week_end: '2024-01-07',
method: 'heuristic',
subject: 'Your Weekly Financial Digest',
greeting: 'Here is your weekly summary!',
highlights: ['Great week!', 'Spending decreased by 15%'],
insights: ['Highest spend in category 1'],
warnings: [],
tips: ['Review top spending categories'],
closing: 'Keep up the good work!',
week_data: {
week_start: '2024-01-01',
week_end: '2024-01-07',
total_income: 2000,
total_expenses: 1500,
net_flow: 500,
categories: { '1': 500, '2': 300 },
transaction_count: 25,
upcoming_bills: [
{ name: 'Electric', amount: 100, due_date: '2024-01-05' },
],
},
previous_week_data: {
week_start: '2023-12-25',
week_end: '2023-12-31',
total_income: 1800,
total_expenses: 1700,
net_flow: 100,
categories: { '1': 600 },
transaction_count: 20,
upcoming_bills: [],
},
comparison: {
total_income_pct_change: 11.1,
total_expenses_pct_change: -11.8,
net_flow_pct_change: 400,
},
persona: 'default',
}),
}));

vi.mock('@/hooks/use-toast', () => ({
useToast: () => ({ toast: vi.fn() }),
}));

const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});

describe('WeeklyDigest', () => {
beforeEach(() => {
queryClient.clear();
});

it('renders weekly summary correctly', async () => {
render(
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<WeeklyDigest />
</BrowserRouter>
</QueryClientProvider>
);

// Wait for loading to finish
await waitFor(() => {
expect(screen.queryByRole('status')).not.toBeInTheDocument();
});

// Check page title
expect(screen.getByText('Weekly Financial Digest')).toBeInTheDocument();

// Check summary cards
expect(screen.getByText('Net Flow')).toBeInTheDocument();
expect(screen.getByText('Income')).toBeInTheDocument();
expect(screen.getByText('Expenses')).toBeInTheDocument();
expect(screen.getByText('Transactions')).toBeInTheDocument();

// Check sections
expect(screen.getByText('Highlights')).toBeInTheDocument();
expect(screen.getByText('Insights')).toBeInTheDocument();
expect(screen.getByText('Pro Tips for Next Week')).toBeInTheDocument();
expect(screen.getByText('Spending by Category')).toBeInTheDocument();
expect(screen.getByText('Upcoming Bills')).toBeInTheDocument();
});

it('displays currency values correctly', async () => {
render(
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<WeeklyDigest />
</BrowserRouter>
</QueryClientProvider>
);

await waitFor(() => {
expect(screen.queryByRole('status')).not.toBeInTheDocument();
});

// Check for amounts - format depends on currency formatter
expect(screen.getByText(/25/i)).toBeInTheDocument(); // transactions
});

it('has navigation buttons', async () => {
render(
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<WeeklyDigest />
</BrowserRouter>
</QueryClientProvider>
);

await waitFor(() => {
expect(screen.queryByRole('status')).not.toBeInTheDocument();
});

expect(screen.getByText('Previous')).toBeInTheDocument();
expect(screen.getByText('Next')).toBeInTheDocument();
expect(screen.getByText('Email Me')).toBeInTheDocument();
});
});
75 changes: 75 additions & 0 deletions app/src/api/weeklyDigest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* 每周财务摘要 API
*/
import { apiClient } from './client';

export interface WeeklyDigestSummary {
week_start: string;
week_end: string;
method: 'gemini' | 'heuristic';
subject: string;
greeting: string;
highlights: string[];
insights: string[];
warnings: string[];
tips: string[];
closing: string;
week_data: WeekData;
previous_week_data: WeekData;
comparison: WeekComparison;
persona: string;
}

export interface WeekData {
week_start: string;
week_end: string;
total_income: number;
total_expenses: number;
net_flow: number;
categories: Record<string, number>;
transaction_count: number;
upcoming_bills: Array<{
name: string;
amount: number;
due_date: string;
}>;
}

export interface WeekComparison {
total_income_pct_change: number;
total_expenses_pct_change: number;
net_flow_pct_change: number;
}

/**
* 获取每周财务摘要
* @param weekStart 周开始日期 (YYYY-MM-DD),默认本周一
*/
export async function getWeeklySummary(weekStart?: string): Promise<WeeklyDigestSummary> {
const params = new URLSearchParams();
if (weekStart) {
params.set('week', weekStart);
}

const queryString = params.toString();
const response = await apiClient.get(
`/weekly-digest/weekly-summary${queryString ? `?${queryString}` : ''}`
);
return response.data;
}

/**
* 发送周摘要邮件
* @param weekStart 周开始日期 (YYYY-MM-DD)
*/
export async function sendSummaryEmail(weekStart?: string): Promise<{
sent: boolean;
reason?: string;
recipient?: string;
summary: WeeklyDigestSummary;
}> {
const response = await apiClient.post('/weekly-digest/weekly-summary/send-email', {
week: weekStart,
});
return response.data;
}
1 change: 1 addition & 0 deletions app/src/components/layout/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { logout as logoutApi } from '@/api/auth';

const navigation = [
{ name: 'Dashboard', href: '/dashboard' },
{ name: 'Weekly Digest', href: '/weekly-digest' },
{ name: 'Budgets', href: '/budgets' },
{ name: 'Bills', href: '/bills' },
{ name: 'Reminders', href: '/reminders' },
Expand Down
Loading