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
132 changes: 118 additions & 14 deletions app/package-lock.json

Large diffs are not rendered by default.

11 changes: 6 additions & 5 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,14 @@
"devDependencies": {
"@eslint/js": "^9.32.0",
"@tailwindcss/typography": "^0.5.16",
"@types/node": "^22.16.5",
"@types/react": "^18.3.23",
"@types/react-dom": "^18.3.7",
"@types/jest": "^29.5.14",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.0.1",
"@testing-library/user-event": "^14.6.1",
"@types/jest": "^29.5.14",
"@types/node": "^22.16.5",
"@types/react": "^18.3.23",
"@types/react-dom": "^18.3.7",
"@vitejs/plugin-react-swc": "^3.11.0",
"autoprefixer": "^10.4.21",
"eslint": "^9.32.0",
Expand All @@ -85,8 +86,8 @@
"jest-environment-jsdom": "^29.7.0",
"jest-junit": "^16.0.0",
"postcss": "^8.5.6",
"ts-jest": "^29.2.5",
"tailwindcss": "^3.4.17",
"ts-jest": "^29.2.5",
"typescript": "^5.8.3",
"typescript-eslint": "^8.38.0",
"vite": "^5.4.19",
Expand Down
72 changes: 69 additions & 3 deletions app/src/api/reminders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ export type Reminder = {
send_at: string; // ISO datetime
sent: boolean;
channel: 'email' | 'whatsapp';
// Delivery tracking fields
delivered?: boolean;
delivery_attempts: number;
last_attempt_at?: string;
error_message?: string;
};

export type ReminderCreate = {
Expand All @@ -16,6 +21,48 @@ export type ReminderCreate = {

export type ReminderUpdate = Partial<ReminderCreate>;

export type ReminderDelivery = {
id: number;
attempted_at: string;
success: boolean;
channel: string;
error_message?: string;
response_time_ms?: number;
};

export type DeliveryMetrics = {
period_days: number;
total_attempts: number;
successful_deliveries: number;
failed_deliveries: number;
success_rate: number;
average_response_time_ms: number;
by_channel?: {
email: {
attempts: number;
successful: number;
success_rate: number;
};
whatsapp: {
attempts: number;
successful: number;
success_rate: number;
};
};
};

export type RunDueResult = {
processed: number;
delivered: number;
failed: number;
};

export type RetryResult = {
success: boolean;
delivered?: boolean;
attempts: number;
};

export async function listReminders(): Promise<Reminder[]> {
return api<Reminder[]>('/reminders');
}
Expand All @@ -28,10 +75,29 @@ export async function updateReminder(id: number, payload: ReminderUpdate): Promi
return api<Reminder>(`/reminders/${id}`, { method: 'PATCH', body: payload });
}

export async function deleteReminder(id: number): Promise<{ message?: string } | Record<string, never>> {
export async function deleteReminder(id: number): Promise<{ message?: string }> {
return api(`/reminders/${id}`, { method: 'DELETE' });
}

export async function runDue(): Promise<{ processed?: number } | Record<string, never>> {
return api('/reminders/run', { method: 'POST' });
export async function runDue(): Promise<RunDueResult> {
return api<RunDueResult>('/reminders/run', { method: 'POST' });
}

// New delivery tracking APIs

export async function getDeliveryMetrics(days?: number): Promise<DeliveryMetrics> {
const query = days ? `?days=${days}` : '';
return api<DeliveryMetrics>(`/reminders/metrics${query}`);
}

export async function getFailedReminders(): Promise<Reminder[]> {
return api<Reminder[]>('/reminders/failed');
}

export async function retryReminder(id: number): Promise<RetryResult> {
return api<RetryResult>(`/reminders/${id}/retry`, { method: 'POST' });
}

export async function getReminderDeliveries(id: number): Promise<ReminderDelivery[]> {
return api<ReminderDelivery[]>(`/reminders/${id}/deliveries`);
}
2 changes: 1 addition & 1 deletion app/tsconfig.app.tsbuildinfo
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"root":["./src/app.tsx","./src/main.tsx","./src/setuptests.ts","./src/vite-env.d.ts","./src/__tests__/dashboard.integration.test.tsx","./src/__tests__/expenses.integration.test.tsx","./src/__tests__/navbar.test.tsx","./src/__tests__/protectedroute.test.tsx","./src/__tests__/signin.test.tsx","./src/__tests__/apiclient.test.ts","./src/__tests__/auth.test.ts","./src/api/auth.ts","./src/api/bills.ts","./src/api/categories.ts","./src/api/client.ts","./src/api/dashboard.ts","./src/api/expenses.ts","./src/api/reminders.ts","./src/components/auth/protectedroute.tsx","./src/components/layout/footer.tsx","./src/components/layout/layout.tsx","./src/components/layout/navbar.tsx","./src/components/ui/accordion.tsx","./src/components/ui/alert-dailog.tsx","./src/components/ui/alert.tsx","./src/components/ui/aspect-ratio.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/breadcrumb.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/carousel.tsx","./src/components/ui/chart.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/collapsible.tsx","./src/components/ui/command.tsx","./src/components/ui/context-menu.tsx","./src/components/ui/dialog.tsx","./src/components/ui/drawer.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/financial-card.tsx","./src/components/ui/form.tsx","./src/components/ui/hover-card.tsx","./src/components/ui/input-otp.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/menubar.tsx","./src/components/ui/navigation-menu.tsx","./src/components/ui/pagination.tsx","./src/components/ui/popover.tsx","./src/components/ui/progress.tsx","./src/components/ui/radio-group.tsx","./src/components/ui/resizable.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/slider.tsx","./src/components/ui/sonner.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/components/ui/toast.tsx","./src/components/ui/toaster.tsx","./src/components/ui/toggle-group.tsx","./src/components/ui/toggle.tsx","./src/components/ui/tooltip.tsx","./src/components/ui/use-toast.tsx","./src/hooks/use-mobile.tsx","./src/hooks/use-toast.ts","./src/lib/auth.ts","./src/lib/utils.ts","./src/pages/analytics.tsx","./src/pages/bills.tsx","./src/pages/budgets.tsx","./src/pages/categories.tsx","./src/pages/dashboard.tsx","./src/pages/expenses.tsx","./src/pages/index.tsx","./src/pages/landing.tsx","./src/pages/notfound.tsx","./src/pages/register.tsx","./src/pages/reminders.tsx","./src/pages/signin.tsx"],"version":"5.8.3"}
{"root":["./src/App.tsx","./src/main.tsx","./src/setupTests.ts","./src/vite-env.d.ts","./src/__tests__/Dashboard.integration.test.tsx","./src/__tests__/Expenses.integration.test.tsx","./src/__tests__/Navbar.test.tsx","./src/__tests__/ProtectedRoute.test.tsx","./src/__tests__/SignIn.test.tsx","./src/__tests__/WeeklyDigest.test.tsx","./src/__tests__/apiClient.test.ts","./src/__tests__/auth.test.ts","./src/api/auth.ts","./src/api/bills.ts","./src/api/categories.ts","./src/api/client.ts","./src/api/dashboard.ts","./src/api/expenses.ts","./src/api/reminders.ts","./src/api/weekly-summary.ts","./src/components/WeeklyDigest.tsx","./src/components/auth/ProtectedRoute.tsx","./src/components/layout/Footer.tsx","./src/components/layout/Layout.tsx","./src/components/layout/Navbar.tsx","./src/components/ui/accordion.tsx","./src/components/ui/alert-dailog.tsx","./src/components/ui/alert.tsx","./src/components/ui/aspect-ratio.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/breadcrumb.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/carousel.tsx","./src/components/ui/chart.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/collapsible.tsx","./src/components/ui/command.tsx","./src/components/ui/context-menu.tsx","./src/components/ui/dialog.tsx","./src/components/ui/drawer.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/financial-card.tsx","./src/components/ui/form.tsx","./src/components/ui/hover-card.tsx","./src/components/ui/input-otp.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/menubar.tsx","./src/components/ui/navigation-menu.tsx","./src/components/ui/pagination.tsx","./src/components/ui/popover.tsx","./src/components/ui/progress.tsx","./src/components/ui/radio-group.tsx","./src/components/ui/resizable.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/slider.tsx","./src/components/ui/sonner.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/components/ui/toast.tsx","./src/components/ui/toaster.tsx","./src/components/ui/toggle-group.tsx","./src/components/ui/toggle.tsx","./src/components/ui/tooltip.tsx","./src/components/ui/use-toast.tsx","./src/hooks/use-mobile.tsx","./src/hooks/use-toast.ts","./src/lib/auth.ts","./src/lib/currency.ts","./src/lib/utils.ts","./src/pages/Account.tsx","./src/pages/Analytics.tsx","./src/pages/Bills.tsx","./src/pages/Budgets.tsx","./src/pages/Categories.tsx","./src/pages/Dashboard.tsx","./src/pages/Expenses.tsx","./src/pages/Index.tsx","./src/pages/Landing.tsx","./src/pages/NotFound.tsx","./src/pages/Register.tsx","./src/pages/Reminders.tsx","./src/pages/SignIn.tsx"],"errors":true,"version":"5.8.3"}
177 changes: 177 additions & 0 deletions docs/reminder-reliability-tracking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# Reminder Reliability Tracking

This document describes the reminder delivery tracking and reliability metrics system implemented in FinMind.

## Overview

The reminder reliability tracking system provides:

- **Delivery Status Tracking**: Know whether reminders were successfully delivered
- **Retry Logic**: Automatic retries with exponential backoff for failed deliveries
- **Reliability Metrics**: Track success rates, response times, and channel performance
- **Failure Management**: View and manually retry failed reminders

## Features

### 1. Delivery Tracking

Each reminder now tracks:
- `delivered`: Boolean indicating delivery success/failure
- `delivery_attempts`: Number of delivery attempts made
- `last_attempt_at`: Timestamp of last delivery attempt
- `error_message`: Error details if delivery failed

### 2. Automatic Retry Logic

Failed deliveries are automatically retried with exponential backoff:
- **Attempt 1**: Immediate
- **Attempt 2**: 5 minutes delay
- **Attempt 3**: 15 minutes delay
- **Attempt 4+**: 60 minutes delay (if extended)

After 3 failed attempts, the reminder is marked as failed and requires manual retry.

### 3. Delivery History

Each delivery attempt is recorded in the `reminder_deliveries` table with:
- Success/failure status
- Channel used (email/WhatsApp)
- Response time (latency)
- Error messages
- Timestamp

## API Endpoints

### Get Delivery Metrics
```
GET /api/reminders/metrics?days=30
```

Returns reliability metrics for the authenticated user:
```json
{
"period_days": 30,
"total_attempts": 100,
"successful_deliveries": 95,
"failed_deliveries": 5,
"success_rate": 95.0,
"average_response_time_ms": 250,
"by_channel": {
"email": {
"attempts": 80,
"successful": 78,
"success_rate": 97.5
},
"whatsapp": {
"attempts": 20,
"successful": 17,
"success_rate": 85.0
}
}
}
```

### Get Failed Reminders
```
GET /api/reminders/failed?limit=10
```

Returns reminders that exhausted all retry attempts.

### Retry a Failed Reminder
```
POST /api/reminders/{id}/retry
```

Manually retry a failed reminder immediately.

### Get Reminder Delivery History
```
GET /api/reminders/{id}/deliveries
```

Returns the complete delivery attempt history for a specific reminder.

### Process Due Reminders (Updated)
```
POST /api/reminders/run
```

Now returns detailed results:
```json
{
"processed": 5,
"delivered": 4,
"failed": 1
}
```

## Database Schema

### Updated `reminders` Table
```sql
ALTER TABLE reminders ADD COLUMN delivered BOOLEAN;
ALTER TABLE reminders ADD COLUMN delivery_attempts INTEGER DEFAULT 0;
ALTER TABLE reminders ADD COLUMN last_attempt_at TIMESTAMP;
ALTER TABLE reminders ADD COLUMN error_message VARCHAR(500);
ALTER TABLE reminders ADD COLUMN created_at TIMESTAMP DEFAULT NOW();
```

### New `reminder_deliveries` Table
```sql
CREATE TABLE reminder_deliveries (
id SERIAL PRIMARY KEY,
reminder_id INTEGER REFERENCES reminders(id),
attempted_at TIMESTAMP DEFAULT NOW(),
success BOOLEAN NOT NULL,
channel VARCHAR(20) NOT NULL,
error_message VARCHAR(500),
response_time_ms INTEGER
);
```

## Migration

Run the migration script to add the new schema:

```bash
cd packages/backend
python migrations/add_reminder_delivery_tracking.py
```

Or if using Alembic:
```bash
alembic upgrade add_reminder_delivery_tracking
```

## Testing

Run the reminder delivery tests:

```bash
cd packages/backend
pytest app/tests/test_reminder_delivery.py -v
```

Tests cover:
- Email/WhatsApp delivery success and failure
- Retry logic with exponential backoff
- Metrics calculation
- Failed reminder retrieval
- Manual retry functionality

## Monitoring

To monitor reminder reliability:

1. **Check metrics regularly** via the `/api/reminders/metrics` endpoint
2. **Review failed reminders** via `/api/reminders/failed`
3. **Set up alerts** when success rate drops below a threshold (e.g., 90%)

## Future Enhancements

Potential improvements:
- Webhook notifications for delivery failures
- Email alerts to admins when reliability drops
- Dashboard visualization of metrics
- Export metrics to external monitoring (Prometheus/Grafana)
18 changes: 18 additions & 0 deletions packages/backend/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,24 @@ class Reminder(db.Model):
send_at = db.Column(db.DateTime, nullable=False)
sent = db.Column(db.Boolean, default=False, nullable=False)
channel = db.Column(db.String(20), default="email", nullable=False)
# Delivery tracking fields
delivered = db.Column(db.Boolean, default=None, nullable=True)
delivery_attempts = db.Column(db.Integer, default=0, nullable=False)
last_attempt_at = db.Column(db.DateTime, nullable=True)
error_message = db.Column(db.String(500), nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)


class ReminderDelivery(db.Model):
"""Track individual delivery attempts for reliability metrics."""
__tablename__ = "reminder_deliveries"
id = db.Column(db.Integer, primary_key=True)
reminder_id = db.Column(db.Integer, db.ForeignKey("reminders.id"), nullable=False)
attempted_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
success = db.Column(db.Boolean, nullable=False)
channel = db.Column(db.String(20), nullable=False)
error_message = db.Column(db.String(500), nullable=True)
response_time_ms = db.Column(db.Integer, nullable=True) # Delivery latency


class AdImpression(db.Model):
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 .bank_sync import bp as bank_sync_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(bank_sync_bp, url_prefix="/bank-sync")
Loading