Merge pull request 'Added initial project goals and guidelines' (#1) from doc into master

Reviewed-on: finwise/finewise#1
This commit was merged in pull request #1.
This commit is contained in:
2026-02-13 21:35:44 +08:00
8 changed files with 6098 additions and 0 deletions

119
doc/README.md Normal file
View File

@@ -0,0 +1,119 @@
# Finance Tracking App - Overview
## Project Description
A comprehensive finance planning and tracking application built with Tauri for cross-platform support (iOS, Android, and Web). The app focuses on expense tracking, receipt recognition, multi-currency support, and financial goal management.
## Target Platforms
- **iOS** (Primary)
- **Android** (Primary)
- **Web** (via Tauri Web target)
## Core Value Propositions
1. **Precision Financial Tracking**: 8-decimal precision using rust_decimal
2. **Multi-Account Management**: Track multiple accounts (bank, cash, digital wallets)
3. **Smart Automation**: Scheduled transactions, auto-contributions to goals
4. **Receipt Intelligence**: OCR-based receipt data extraction
5. **Goal-Oriented**: Visual goal tracking with automatic contributions
6. **Multi-Currency**: Full support for international transactions
7. **Offline-First**: Works without internet, sync-ready for future cloud features
## User Personas
### Primary: Hong Kong Professional
- Manages multiple bank accounts (HSBC, Hang Seng, etc.)
- Uses digital wallets (PayMe, AlipayHK, Octopus)
- Travels frequently (multi-currency needs)
- Wants to track spending and save for goals
- Prefers English or Traditional Chinese interface
### Secondary: Multi-Account User
- Has checking, savings, credit cards, and cash
- Wants combined view of all finances
- Needs budget tracking across accounts
- Wants to track loans and liabilities
## Success Metrics
- Complete transaction entry in < 30 seconds
- Receipt processing accuracy > 90%
- App launch time < 3 seconds
- Works offline 100%
## Document Structure
This documentation is organized into:
1. **architecture.md** - System architecture and data flow
2. **database.md** - Complete database schema (SeaORM)
3. **features.md** - Feature specifications and workflows
4. **ui-design.md** - UI/UX design guidelines
5. **technical-specs.md** - Technical implementation details
6. **api-reference.md** - Tauri commands and data structures
7. **validation.md** - Input validation (validator crate + Zod)
## Key Design Decisions
### Precision
- All financial amounts stored with 8 decimal precision
- Use rust_decimal for accurate calculations
- Never use floating-point for money
### Time Handling
- All datetimes stored in UTC
- Display in user's local timezone
- User-adjustable timezone in settings
### Categories
- Flat tag-based system (no hierarchy)
- Multiple tags per transaction
- Budgets based on tag combinations
### Scheduled Transactions
- Auto-insert on due date at configurable time (default 00:00)
- Marked as "needs review" when auto-inserted
- Support for complex recurrence patterns
### Goals
- Real-time auto-contribution when transactions added
- Contribution rules based on transaction tags
- Visual progress tracking
### Multi-Device Ready
- UUID-based identifiers
- Version tracking for conflict resolution
- Soft deletes for sync compatibility
## Future Roadmap (Post-MVP)
1. Cloud sync across devices
2. Bank import integration (HSBC, Hang Seng APIs)
3. Investment account support
4. Advanced analytics and reporting
5. Shared accounts (family/partner)
6. Bill reminders and notifications
## Team Structure
**2-Person Team:**
- **Person A**: Rust backend, database, business logic
- **Person B**: React frontend, UI/UX, mobile optimization
## Timeline
- **MVP Duration**: 8-10 weeks
- **Phase 1** (Weeks 1-2): Foundation
- **Phase 2** (Weeks 3-4): Core Features
- **Phase 3** (Weeks 5-6): Advanced Features
- **Phase 4** (Weeks 7-8): Polish & Testing
- **Phase 5** (Weeks 9-10): Release
## License
TBD - Internal use for now
## Contact
TBD

922
doc/api-reference.md Normal file
View File

@@ -0,0 +1,922 @@
# API Reference
## Overview
This document provides detailed API specifications for all Tauri commands.
## Common Types
### Money
```typescript
interface Money {
amount: string; // 8 decimal places: "1234.56000000"
currency: Currency;
}
```
### Currency Enum
```typescript
enum Currency {
HKD = 'HKD',
USD = 'USD',
CNY = 'CNY',
EUR = 'EUR',
GBP = 'GBP',
JPY = 'JPY',
BTC = 'BTC',
ETH = 'ETH',
}
```
### TransactionType Enum
```typescript
enum TransactionType {
EXPENSE = 'expense',
INCOME = 'income',
TRANSFER_OUT = 'transfer_out',
TRANSFER_IN = 'transfer_in',
}
```
### AccountType Enum
```typescript
enum AccountType {
CHECKING = 'checking',
SAVINGS = 'savings',
CREDIT_CARD = 'credit_card',
CASH = 'cash',
DIGITAL_WALLET = 'digital_wallet',
LOAN = 'loan',
OTHER = 'other',
}
```
---
## Account Commands
### `create_account`
Creates a new financial account.
**Parameters:**
```typescript
{
name: string; // Account display name
account_type: AccountType; // Type of account
currency: Currency; // Account currency
initial_balance: string; // Starting balance (8 decimals)
color?: string; // Hex color (default: #3B82F6)
icon?: string; // Lucide icon name (default: wallet)
}
```
**Returns:** `Account`
**Example:**
```typescript
const account = await invoke('create_account', {
name: 'HSBC Checking',
account_type: 'checking',
currency: 'HKD',
initial_balance: '10000.00000000',
color: '#3B82F6',
icon: 'building-2'
});
```
---
### `get_accounts`
Retrieves all accounts.
**Parameters:**
```typescript
{
include_archived?: boolean; // Include archived accounts (default: false)
}
```
**Returns:** `Account[]`
**Example:**
```typescript
const accounts = await invoke('get_accounts', {
include_archived: false
});
```
---
### `get_account`
Retrieves a single account by ID.
**Parameters:**
```typescript
{
id: string; // Account UUID
}
```
**Returns:** `Account`
---
### `update_account`
Updates an existing account.
**Parameters:**
```typescript
{
id: string;
updates: {
name?: string;
color?: string;
icon?: string;
sort_order?: number;
is_active?: boolean;
include_in_net_worth?: boolean;
show_in_combined_view?: boolean;
}
}
```
**Returns:** `Account`
---
### `archive_account`
Archives or unarchives an account.
**Parameters:**
```typescript
{
id: string;
archived: boolean; // true to archive, false to unarchive
}
```
**Returns:** `void`
---
### `delete_account`
Soft-deletes an account (preserves history).
**Parameters:**
```typescript
{
id: string;
}
```
**Returns:** `void`
---
### `get_account_balance`
Calculates account balance.
**Parameters:**
```typescript
{
id: string;
as_of_date?: string; // Optional: balance as of specific date (YYYY-MM-DD)
}
```
**Returns:** `Money`
---
## Transaction Commands
### `create_transaction`
Creates a new transaction.
**Parameters:**
```typescript
{
account_id: string;
transaction_type: TransactionType;
gross_amount: string; // Pre-tax amount (8 decimals)
tax_amount?: string; // Tax portion (default: 0)
net_amount: string; // Post-tax amount (8 decimals)
currency: Currency;
description: string;
merchant?: string;
notes?: string;
tag_ids: string[]; // Tag UUIDs
transaction_date: string; // YYYY-MM-DD
receipt_paths?: string[]; // Local file paths
}
```
**Returns:** `Transaction`
**Example:**
```typescript
const transaction = await invoke('create_transaction', {
account_id: 'acc-uuid',
transaction_type: 'expense',
gross_amount: '120.00000000',
net_amount: '120.00000000',
currency: 'HKD',
description: 'Lunch at McDonald\'s',
merchant: 'McDonald\'s',
tag_ids: ['tag-food', 'tag-dining'],
transaction_date: '2026-02-13'
});
```
---
### `get_transactions`
Retrieves transactions with filtering.
**Parameters:**
```typescript
{
filter: {
account_id?: string; // Filter by account
transaction_type?: TransactionType;
tag_ids?: string[]; // Filter by tags (AND logic)
start_date?: string; // YYYY-MM-DD
end_date?: string; // YYYY-MM-DD
search_query?: string; // Search in description, merchant
needs_review?: boolean; // Filter by review status
is_auto_inserted?: boolean; // Filter auto-inserted
limit?: number; // Pagination limit
offset?: number; // Pagination offset
sort_by?: 'date' | 'amount' | 'created_at';
sort_order?: 'asc' | 'desc';
}
}
```
**Returns:** `Transaction[]`
**Example:**
```typescript
const transactions = await invoke('get_transactions', {
filter: {
account_id: 'acc-uuid',
start_date: '2026-02-01',
end_date: '2026-02-28',
tag_ids: ['tag-food'],
sort_by: 'date',
sort_order: 'desc',
limit: 50
}
});
```
---
### `get_transaction`
Retrieves a single transaction.
**Parameters:**
```typescript
{
id: string;
}
```
**Returns:** `Transaction`
---
### `update_transaction`
Updates an existing transaction.
**Parameters:**
```typescript
{
id: string;
updates: {
gross_amount?: string;
tax_amount?: string;
net_amount?: string;
description?: string;
merchant?: string;
notes?: string;
tag_ids?: string[];
transaction_date?: string;
receipt_paths?: string[];
}
}
```
**Returns:** `Transaction`
**Notes:**
- Updates account balance
- Recalculates goal contributions
- Marks reconciliation as needs review
---
### `delete_transaction`
Soft-deletes a transaction.
**Parameters:**
```typescript
{
id: string;
}
```
**Returns:** `void`
---
### `get_transactions_needing_review`
Gets all auto-inserted transactions needing user review.
**Parameters:** None
**Returns:** `Transaction[]`
---
### `confirm_transaction`
Confirms an auto-inserted transaction (removes needs_review flag).
**Parameters:**
```typescript
{
id: string;
}
```
**Returns:** `void`
---
## Tag Commands
### `create_tag`
Creates a new tag.
**Parameters:**
```typescript
{
name: string; // Unique name
color: string; // Hex color
icon?: string; // Lucide icon name
budget_amount?: string; // Optional budget
budget_period?: 'daily' | 'weekly' | 'monthly' | 'yearly';
}
```
**Returns:** `Tag`
---
### `get_tags`
Retrieves all tags.
**Parameters:**
```typescript
{
include_system?: boolean; // Include built-in tags (default: true)
}
```
**Returns:** `Tag[]`
---
### `update_tag`
Updates a tag.
**Parameters:**
```typescript
{
id: string;
updates: {
name?: string;
color?: string;
icon?: string;
budget_amount?: string;
budget_period?: 'daily' | 'weekly' | 'monthly' | 'yearly';
}
}
```
**Returns:** `Tag`
---
### `delete_tag`
Deletes a tag (removes from all transactions).
**Parameters:**
```typescript
{
id: string;
}
```
**Returns:** `void`
---
## Scheduled Transaction Commands
### `create_scheduled_transaction`
Creates a recurring transaction template.
**Parameters:**
```typescript
{
account_id: string;
schedule_type: 'daily' | 'weekly' | 'monthly' | 'yearly' | 'custom';
frequency: number; // Every N days/weeks/months
days_of_week?: number[]; // For weekly: [0, 1] = Sun, Mon
day_of_month?: number; // For monthly: 1-31, -1 = last day
month_of_year?: number; // For yearly: 1-12
execution_time: string; // HH:MM format (default: "00:00")
timezone?: string; // IANA timezone
start_date: string; // YYYY-MM-DD
end_date?: string; // Optional end date
occurrence_count?: number; // Optional: stop after N occurrences
transaction_type: TransactionType;
gross_amount: string;
tax_amount?: string;
net_amount: string;
currency: Currency;
description?: string;
merchant?: string;
notes?: string;
tag_ids: string[];
}
```
**Returns:** `ScheduledTransaction`
**Example:**
```typescript
const schedule = await invoke('create_scheduled_transaction', {
account_id: 'acc-uuid',
schedule_type: 'monthly',
frequency: 1,
day_of_month: 1,
execution_time: '09:00',
start_date: '2026-03-01',
transaction_type: 'expense',
gross_amount: '15000.00000000',
net_amount: '15000.00000000',
currency: 'HKD',
description: 'Monthly Rent',
tag_ids: ['tag-housing']
});
```
---
### `get_scheduled_transactions`
Retrieves scheduled transactions.
**Parameters:**
```typescript
{
include_inactive?: boolean; // Include paused schedules (default: false)
}
```
**Returns:** `ScheduledTransaction[]`
---
### `update_scheduled_transaction`
Updates a scheduled transaction.
**Parameters:**
```typescript
{
id: string;
updates: {
schedule_type?: string;
frequency?: number;
execution_time?: string;
end_date?: string;
occurrence_count?: number;
is_active?: boolean;
// ... other fields
}
}
```
**Returns:** `ScheduledTransaction`
**Notes:** Changes only affect future instances.
---
### `delete_scheduled_transaction`
Deletes a scheduled transaction.
**Parameters:**
```typescript
{
id: string;
}
```
**Returns:** `void`
---
### `skip_scheduled_instance`
Skips a specific instance of a scheduled transaction.
**Parameters:**
```typescript
{
schedule_id: string;
due_date: string; // YYYY-MM-DD
}
```
**Returns:** `void`
---
### `process_due_schedules`
Manually triggers processing of due scheduled transactions.
**Parameters:** None
**Returns:** `Transaction[]` // Generated transactions
---
## Goal Commands
### `create_goal`
Creates a new financial goal.
**Parameters:**
```typescript
{
name: string;
description?: string;
target_amount: string; // Target amount (8 decimals)
currency: Currency;
goal_type: 'savings' | 'debt_payoff' | 'spending_limit' | 'custom';
target_date?: string; // Optional deadline (YYYY-MM-DD)
is_recurring?: boolean; // Reset on period
linked_account_id?: string; // Optional linked account
color?: string; // Hex color
icon?: string; // Lucide icon name
}
```
**Returns:** `Goal`
**Example:**
```typescript
const goal = await invoke('create_goal', {
name: 'Japan Vacation',
target_amount: '50000.00000000',
currency: 'HKD',
goal_type: 'savings',
target_date: '2026-12-31',
color: '#F59E0B',
icon: 'plane'
});
```
---
### `get_goals`
Retrieves goals.
**Parameters:**
```typescript
{
include_achieved?: boolean; // Include completed goals (default: false)
}
```
**Returns:** `Goal[]`
---
### `update_goal`
Updates a goal.
**Parameters:**
```typescript
{
id: string;
updates: {
name?: string;
description?: string;
target_amount?: string;
target_date?: string;
is_active?: boolean;
color?: string;
icon?: string;
}
}
```
**Returns:** `Goal`
---
### `delete_goal`
Deletes a goal.
**Parameters:**
```typescript
{
id: string;
}
```
**Returns:** `void`
---
### `create_goal_rule`
Creates an auto-contribution rule for a goal.
**Parameters:**
```typescript
{
goal_id: string;
tag_ids: string[]; // Match if transaction has ALL tags
contribution_type: 'percentage' | 'fixed';
percentage?: string; // e.g., "10.00" for 10%
fixed_amount?: string; // e.g., "5.00000000"
max_contribution_per_transaction?: string;
monthly_cap?: string;
}
```
**Returns:** `GoalRule`
**Example:**
```typescript
const rule = await invoke('create_goal_rule', {
goal_id: 'goal-uuid',
tag_ids: ['tag-food'],
contribution_type: 'percentage',
percentage: '10.00',
monthly_cap: '500.00000000'
});
```
---
### `manual_contribute_to_goal`
Manually adds contribution to a goal.
**Parameters:**
```typescript
{
goal_id: string;
amount: string; // Can be negative (withdrawal)
notes?: string;
}
```
**Returns:** `void`
---
## Transfer Commands
### `create_transfer`
Creates a transfer between accounts.
**Parameters:**
```typescript
{
from_account_id: string;
to_account_id: string;
from_amount: string; // Amount in source currency
to_amount: string; // Amount in destination currency
exchange_rate?: string; // Optional: custom rate
exchange_rate_source?: 'auto' | 'manual' | 'api';
fees?: string; // Transfer fees
description?: string;
transfer_date: string; // YYYY-MM-DD
}
```
**Returns:** `Transfer`
**Notes:** Auto-creates paired transactions in both accounts.
---
### `get_transfers`
Retrieves transfers.
**Parameters:**
```typescript
{
account_id?: string; // Filter by account (either from or to)
start_date?: string;
end_date?: string;
}
```
**Returns:** `Transfer[]`
---
## Settings Commands
### `get_settings`
Retrieves all application settings.
**Parameters:** None
**Returns:** `Settings`
```typescript
interface Settings {
language: 'en' | 'zh-Hant';
default_currency: Currency;
base_currency: Currency;
timezone: string;
default_view: 'combined' | 'single';
display_mode: 'net' | 'gross';
decimal_places: number;
date_format: string;
time_format: '12h' | '24h';
theme: 'light' | 'dark' | 'system';
week_starts_on: 0 | 1;
scheduled_check_interval: number;
}
```
---
### `update_setting`
Updates a single setting.
**Parameters:**
```typescript
{
key: string; // Setting key
value: string; // Setting value (JSON stringified if object)
}
```
**Returns:** `void`
---
### `update_settings`
Updates multiple settings at once.
**Parameters:**
```typescript
{
settings: Partial<Settings>;
}
```
**Returns:** `void`
---
## Backup Commands
### `export_to_json`
Exports all data to JSON file.
**Parameters:**
```typescript
{
path: string; // Full file path for export
}
```
**Returns:** `void`
---
### `import_from_json`
Imports data from JSON file.
**Parameters:**
```typescript
{
path: string; // Full file path to import
}
```
**Returns:** `ImportReport`
```typescript
interface ImportReport {
success: boolean;
accounts_imported: number;
transactions_imported: number;
goals_imported: number;
errors: string[];
}
```
---
### `get_database_path`
Returns the path to the SQLite database file.
**Parameters:** None
**Returns:** `string`
---
## Error Responses
All commands return errors as strings:
```typescript
try {
const result = await invoke('create_account', params);
} catch (error) {
// error is a string message
console.error(error); // "Database error: ..." or "Validation error: ..."
}
```
### Common Error Types
- `Database error: {message}` - SQLite errors
- `Validation error: {message}` - Invalid input
- `Not found: {entity} with id {id}` - Entity doesn't exist
- `Invalid amount: {amount}` - Bad decimal format
- `Currency mismatch: expected {expected}, got {actual}` - Currency conflict
- `IO error: {message}` - File system errors
---
## Event Listeners (Future)
For real-time updates (when implemented):
```typescript
// Listen for auto-inserted transactions
listen('transaction:auto_inserted', (event) => {
const transaction = event.payload;
showNotification(`New transaction needs review: ${transaction.description}`);
});
// Listen for goal achievements
listen('goal:achieved', (event) => {
const goal = event.payload;
showCelebration(`Goal achieved: ${goal.name}!`);
});
```
---
**Version**: 1.0.0
**Last Updated**: 2026-02-13

523
doc/architecture.md Normal file
View File

@@ -0,0 +1,523 @@
# System Architecture
## High-Level Architecture
```
┌────────────────────────────────────────────────────────────┐
│ User Interface Layer │
│ (React + TypeScript + shadcn/ui) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ iOS App │ │ Android │ │ Web │ │ Desktop │ │
│ │ (Tauri) │ │ (Tauri) │ │ (Tauri) │ │ (Tauri) │ │
│ └────┬─────┘ └─────┬────┘ └─────┬────┘ └─────┬────┘ │
└───────┼──────────────┼─────────────┼─────────────┼─────────┘
│ │ │ │
└──────────────┴─────────────┴─────────────┘
┌───────────┴───────────┐
│ Tauri Bridge/API │
└───────────┬───────────┘
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Database │ │ File System│ │ Camera │
│ (SQLite) │ │ (Receipts) │ │ (OCR) │
└──────────────┘ └──────────────┘ └──────────────┘
```
## Technology Stack
### Frontend (React + TypeScript)
**Core Framework:**
- React 18 with hooks
- TypeScript for type safety
- Vite for fast development
**UI Components:**
- shadcn/ui component library
- Tailwind CSS for styling
- Lucide React for icons
- Framer Motion for animations
**State Management:**
- TanStack Query (React Query) for server state
- Zustand for client state
**Utilities:**
- react-i18next for internationalization (English + Traditional Chinese)
- date-fns for date manipulation
- zod for validation
- react-hook-form for forms
### Backend (Rust + Tauri)
**Core:**
- Tauri v2 for cross-platform
- tokio for async runtime
- serde for serialization
**Financial:**
- rust_decimal for 8-decimal precision
- rust_decimal_macros for convenience
**Database:**
- SeaORM for SQLite
- sea-orm-cli for migrations
- Async database operations
**Validation:**
- validator crate for input validation
- Validate DTOs before database operations
**Time:**
- chrono for date/time handling
- Always store UTC, convert to local for display
## Data Flow
### Transaction Creation Flow
```
1. User fills form → React state
2. Frontend Validation (Zod schema)
3. Call Tauri command (invoke)
4. Backend Validation (validator crate DTO)
5. Save to SQLite (SeaORM)
6. Process goal contributions (if expense)
7. Update account balance
8. Return transaction to frontend
9. React Query updates cache
10. UI re-renders
11. Show success toast
```
### Scheduled Transaction Execution
```
Background Timer (every minute check)
Query scheduled_transactions table
Find transactions where next_execution_datetime <= now
For each due schedule:
├─ Create transaction (is_auto_inserted = true)
├─ Mark as needs_review
├─ Update schedule next date
├─ Save to database
└─ Update UI (if app is open)
```
### Goal Contribution Flow
```
Transaction Saved
Check transaction tags
Query goal_rules for matching tags
For each matching rule:
├─ Calculate contribution (percentage or fixed)
├─ Update goal.current_amount
├─ Create goal_progress record
└─ Check if goal achieved
Return updated goals to UI
Show toast: "Added $X to [Goal Name]"
```
## Project Structure
```
finance-tracker/
├── src/ # Frontend source
│ ├── components/ # Reusable UI components
│ │ ├── ui/ # shadcn/ui components
│ │ ├── forms/ # Form components
│ │ ├── layout/ # Layout components
│ │ └── charts/ # Chart/visualization
│ ├── pages/ # Screen/page components
│ │ ├── Dashboard.tsx
│ │ ├── Transactions.tsx
│ │ ├── Accounts.tsx
│ │ ├── Goals.tsx
│ │ ├── Settings.tsx
│ │ └── ...
│ ├── hooks/ # Custom React hooks
│ │ ├── useTransactions.ts
│ │ ├── useAccounts.ts
│ │ ├── useGoals.ts
│ │ └── ...
│ ├── stores/ # Zustand stores
│ │ ├── settingsStore.ts
│ │ └── uiStore.ts
│ ├── lib/ # Utilities
│ │ ├── utils.ts
│ │ ├── currency.ts # Currency formatting
│ │ ├── validation.ts # Zod schemas
│ │ └── constants.ts
│ ├── types/ # TypeScript types
│ │ ├── models.ts # Data models
│ │ └── api.ts # API types
│ ├── i18n/ # Translations
│ │ ├── en.json
│ │ └── zh-Hant.json
│ ├── App.tsx
│ ├── main.tsx
│ └── index.css
├── src-tauri/ # Tauri backend
│ ├── src/
│ │ ├── main.rs # Entry point
│ │ ├── lib.rs # Library exports
│ │ ├── commands/ # Tauri commands
│ │ │ ├── accounts.rs
│ │ │ ├── transactions.rs
│ │ │ ├── goals.rs
│ │ │ ├── scheduled.rs
│ │ │ └── backup.rs
│ │ ├── db/ # Database module
│ │ │ ├── mod.rs
│ │ │ ├── schema.rs # Table definitions
│ │ │ ├── migrations.rs
│ │ │ └── queries.rs
│ │ ├── models/ # Rust data models
│ │ │ ├── mod.rs
│ │ │ ├── money.rs # Money/Currency types
│ │ │ ├── account.rs
│ │ │ ├── transaction.rs
│ │ │ └── goal.rs
│ │ ├── services/ # Business logic
│ │ │ ├── mod.rs
│ │ │ ├── scheduler.rs # Scheduled transaction engine
│ │ │ ├── goals.rs # Goal contribution logic
│ │ │ └── transfers.rs # Transfer logic
│ │ └── error.rs # Error handling
│ ├── capabilities/ # Permission configs
│ ├── gen/ # Generated mobile files
│ ├── Cargo.toml
│ └── build.rs
├── doc/ # Documentation
├── public/ # Static assets
├── package.json
├── tsconfig.json
├── tailwind.config.js
└── vite.config.ts
```
## Database Architecture
**ORM:** SeaORM for SQLite
**Location:** Platform-specific app data directory
**Encryption:** SQLCipher (optional, via SeaORM)
### Key Principles
1. **SeaORM Entities** - Type-safe database models with derive macros
2. **Async Operations** - All database calls are async
3. **Single database file** per user
4. **Foreign key constraints** enabled
5. **Soft deletes** for sync compatibility
6. **Version tracking** on every table
7. **Audit timestamps** (created_at, updated_at)
### Connection Management
```rust
use sea_orm::{Database, DatabaseConnection};
pub async fn establish_connection() -> Result<DatabaseConnection, DbErr> {
let db_path = get_database_path();
let url = format!("sqlite://{}?mode=rwc", db_path);
Database::connect(&url).await
}
```
### SeaORM Entity Structure
```rust
// src-tauri/src/entities/account.rs
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "accounts")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub name: String,
pub account_type: String,
pub currency: String,
pub initial_balance: String,
pub current_balance: String,
pub color: Option<String>,
pub icon: Option<String>,
pub sort_order: i32,
pub is_active: bool,
pub is_archived: bool,
pub include_in_net_worth: bool,
pub show_in_combined_view: bool,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
pub version: i32,
pub device_id: Option<String>,
pub is_deleted: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::transaction::Entity")]
Transactions,
}
impl Related<super::transaction::Entity> for Entity {
fn to() -> RelationDef {
Relation::Transactions.def()
}
}
impl ActiveModelBehavior for ActiveModel {}
```
### Migrations
```rust
// src-tauri/src/migrations/m001_initial.rs
use sea_orm_migration::prelude::*;
pub struct Migration;
impl MigrationName for Migration {
fn name(&self) -> &str {
"m001_initial"
}
}
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Account::Table)
.if_not_exists()
.col(ColumnDef::new(Account::Id).string().not_null().primary_key())
.col(ColumnDef::new(Account::Name).string().not_null())
.col(ColumnDef::new(Account::AccountType).string().not_null())
.col(ColumnDef::new(Account::Currency).string().not_null())
.col(ColumnDef::new(Account::InitialBalance).string().not_null())
.col(ColumnDef::new(Account::CurrentBalance).string().not_null())
.col(ColumnDef::new(Account::Color).string())
.col(ColumnDef::new(Account::Icon).string())
.col(ColumnDef::new(Account::SortOrder).integer().not_null().default(0))
.col(ColumnDef::new(Account::IsActive).boolean().not_null().default(true))
.col(ColumnDef::new(Account::IsArchived).boolean().not_null().default(false))
.col(ColumnDef::new(Account::IncludeInNetWorth).boolean().not_null().default(true))
.col(ColumnDef::new(Account::ShowInCombinedView).boolean().not_null().default(true))
.col(ColumnDef::new(Account::CreatedAt).date_time().not_null())
.col(ColumnDef::new(Account::UpdatedAt).date_time().not_null())
.col(ColumnDef::new(Account::Version).integer().not_null().default(1))
.col(ColumnDef::new(Account::DeviceId).string())
.col(ColumnDef::new(Account::IsDeleted).boolean().not_null().default(false))
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Account::Table).to_owned())
.await
}
}
```
### Usage Example
```rust
// Query all active accounts
use sea_orm::{entity::*, query::*};
let accounts = Account::find()
.filter(account::Column::IsActive.eq(true))
.filter(account::Column::IsDeleted.eq(false))
.order_by_asc(account::Column::SortOrder)
.all(&db)
.await?;
// Create new account
let account = account::ActiveModel {
id: Set(uuid::Uuid::new_v4().to_string()),
name: Set("HSBC Checking".to_string()),
account_type: Set("checking".to_string()),
currency: Set("HKD".to_string()),
initial_balance: Set("10000.00000000".to_string()),
current_balance: Set("10000.00000000".to_string()),
created_at: Set(Utc::now()),
updated_at: Set(Utc::now()),
..Default::default()
};
let result = account.insert(&db).await?;
// Update with version increment
let mut account: account::ActiveModel = account::Entity::find_by_id(id)
.one(&db)
.await?
.ok_or(DbErr::Custom("Not found".to_string()))?
.into();
account.name = Set("New Name".to_string());
account.version = Set(account.version.unwrap() + 1);
account.updated_at = Set(Utc::now());
let updated = account.update(&db).await?;
```
## Mobile-Specific Considerations
### iOS
- Use iOS Safe Area for notches
- Follow Human Interface Guidelines
- Support both portrait and landscape
- Handle app lifecycle (background/foreground)
### Android
- Material Design 3 principles
- Support system back button
- Handle different screen densities
- Runtime permissions for camera/storage
### Web
- PWA capabilities (optional)
- Responsive breakpoints
- Touch-optimized controls
- LocalStorage for settings only (data in SQLite)
## Security Considerations
1. **Local data only** - No cloud storage in MVP
2. **Database encryption** - Optional SQLCipher
3. **Secure storage** - Use platform keychains for sensitive data
4. **No network calls** - Except for OCR (Google Vision API)
5. **Receipt privacy** - Store locally, no cloud upload
## Performance Targets
- **App launch**: < 3 seconds
- **Transaction save**: < 500ms
- **List loading**: < 1 second for 1000 transactions
- **Search**: < 200ms with debounce
- **Receipt capture**: < 2 seconds processing
## Error Handling Strategy
**Frontend:**
- React Error Boundaries for UI crashes
- Toast notifications for user feedback
- Form validation with Zod
- Retry logic for failed operations
**Backend:**
- Result types for all operations
- Structured error responses
- Transaction rollback on failures
- Graceful degradation for optional features
## Testing Strategy
**Unit Tests:**
- Rust: Business logic tests
- TypeScript: Utility function tests
**Integration Tests:**
- Database operations
- Tauri command tests
**E2E Tests:**
- Critical user flows
- Cross-platform validation
**Manual Testing:**
- iOS device testing
- Android device testing
- Web browser testing
## Build & Deployment
### Development
```bash
# Start dev server
npm run tauri dev
# iOS development
npm run tauri ios dev
# Android development
npm run tauri android dev
```
### Production Builds
```bash
# iOS
npm run tauri ios build
# Android
npm run tauri android build
# Web
npm run tauri build --target web
```
### Distribution
- **iOS**: TestFlight → App Store
- **Android**: Internal Testing → Play Store
- **Web**: Static hosting (GitHub Pages, Netlify, etc.)
## Monitoring & Analytics (Future)
- Crash reporting (Sentry)
- Performance monitoring
- Usage analytics (opt-in only)
- Error tracking
## Backup Strategy
1. **Manual export**: JSON export/import
2. **Auto-backup**: Periodic JSON export to user-selected folder
3. **iCloud/Google Drive**: Future cloud backup option
## Migration Strategy
- Database migrations using sqlx or similar
- Version check on app start
- Automatic migration if needed
- Backup before migration
## Documentation Maintenance
This architecture document should be updated when:
- New major features added
- Technology stack changes
- Significant architectural decisions made
- Performance characteristics change

921
doc/database.md Normal file
View File

@@ -0,0 +1,921 @@
# Database Schema
## Overview
SQLite database with SeaORM ORM. Uses 8-decimal precision for all monetary values (stored as TEXT). All tables include sync-compatible fields (UUIDs, versions, timestamps).
## SeaORM Entities
All database tables are defined as SeaORM entities with derive macros:
```rust
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "accounts")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub name: String,
pub account_type: String,
pub currency: String,
pub initial_balance: String,
pub current_balance: String,
pub color: Option<String>,
pub icon: Option<String>,
pub sort_order: i32,
pub is_active: bool,
pub is_archived: bool,
pub include_in_net_worth: bool,
pub show_in_combined_view: bool,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
pub version: i32,
pub device_id: Option<String>,
pub is_deleted: bool,
}
```
## Data Types
- **TEXT**: Strings, UUIDs, dates (ISO 8601), decimals (stored as strings for precision)
- **INTEGER**: Booleans (0/1), counts, foreign keys
- **JSON**: Arrays and objects (SQLite 3.38+)
## Precision Handling
All monetary amounts stored as TEXT with 8 decimal places:
```
1234.56000000
0.00010000
9999999999.99999999
```
## Complete Schema
### 1. accounts
Multi-account support for tracking different financial accounts.
```sql
CREATE TABLE accounts (
id TEXT PRIMARY KEY, -- UUID v4
name TEXT NOT NULL, -- Display name
account_type TEXT NOT NULL CHECK(account_type IN (
'checking', -- Bank checking account
'savings', -- Bank savings account
'credit_card', -- Credit card (negative = owed)
'cash', -- Physical cash
'digital_wallet', -- PayMe, AlipayHK, Octopus, etc.
'loan', -- Loan/liability tracking
'other' -- Custom account type
)),
-- Currency & Balance (8 decimal precision)
currency TEXT NOT NULL, -- ISO 4217 code: HKD, USD, CNY, etc.
initial_balance TEXT NOT NULL DEFAULT '0.00000000',
current_balance TEXT NOT NULL DEFAULT '0.00000000',
-- Visual Customization
color TEXT DEFAULT '#3B82F6', -- Hex color for UI
icon TEXT DEFAULT 'wallet', -- Lucide icon name
sort_order INTEGER DEFAULT 0, -- Display order
-- Settings
is_active BOOLEAN DEFAULT 1, -- Archived accounts hidden by default
is_archived BOOLEAN DEFAULT 0,
include_in_net_worth BOOLEAN DEFAULT 1, -- Exclude loans from net worth
show_in_combined_view BOOLEAN DEFAULT 1, -- Show in dashboard
-- Sync & Audit
created_at TEXT NOT NULL, -- ISO 8601 UTC
updated_at TEXT NOT NULL,
version INTEGER DEFAULT 1, -- Increment on every change
device_id TEXT, -- Device that last modified
is_deleted BOOLEAN DEFAULT 0, -- Soft delete for sync
-- Constraints
CHECK (initial_balance GLOB '-?[0-9]*.?[0-9]*'),
CHECK (current_balance GLOB '-?[0-9]*.?[0-9]*')
);
```
**Indexes:**
```sql
CREATE INDEX idx_accounts_active ON accounts(is_active, is_archived);
CREATE INDEX idx_accounts_type ON accounts(account_type);
CREATE INDEX idx_accounts_currency ON accounts(currency);
```
### 2. tags
Flat tag-based categorization system.
```sql
CREATE TABLE tags (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE, -- "Food", "Transport", "Salary"
color TEXT NOT NULL, -- Hex color (full color picker)
icon TEXT, -- Lucide icon name
-- Budgeting (optional)
budget_amount TEXT, -- NULL = no budget
budget_period TEXT CHECK(budget_period IN ('daily', 'weekly', 'monthly', 'yearly')),
-- Flags
is_system BOOLEAN DEFAULT 0, -- Built-in tags (protected)
sort_order INTEGER DEFAULT 0,
-- Sync & Audit
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
version INTEGER DEFAULT 1,
device_id TEXT,
is_deleted BOOLEAN DEFAULT 0,
-- Constraints
CHECK (budget_amount GLOB '-?[0-9]*.?[0-9]*' OR budget_amount IS NULL)
);
```
**Indexes:**
```sql
CREATE INDEX idx_tags_active ON tags(is_deleted);
CREATE INDEX idx_tags_system ON tags(is_system);
```
### 3. transactions
Core transaction table for expenses, incomes, and transfers.
```sql
CREATE TABLE transactions (
id TEXT PRIMARY KEY,
account_id TEXT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
-- Transaction Type
transaction_type TEXT NOT NULL CHECK(transaction_type IN (
'expense', -- Money out
'income', -- Money in
'transfer_out', -- Transfer from this account
'transfer_in' -- Transfer to this account
)),
-- Amounts (8 decimal precision)
gross_amount TEXT NOT NULL, -- Pre-tax amount
tax_amount TEXT DEFAULT '0.00000000',
net_amount TEXT NOT NULL, -- Post-tax (actual paid)
tax_rate TEXT, -- e.g., "0.00000000" for HK (no GST)
-- Currency
currency TEXT NOT NULL,
-- Description & Details
description TEXT NOT NULL,
merchant TEXT,
notes TEXT,
-- Receipt
receipt_paths JSON, -- Array of file paths: ["receipts/xxx.jpg"]
receipt_ocr_data JSON, -- Parsed OCR data
-- Transfer Linkage
transfer_id TEXT, -- Links paired transfer records
related_transaction_id TEXT REFERENCES transactions(id),
-- Schedule
schedule_id TEXT REFERENCES scheduled_transactions(id),
is_scheduled_instance BOOLEAN DEFAULT 0,
is_auto_inserted BOOLEAN DEFAULT 0, -- Flag for auto-inserted scheduled transactions
needs_review BOOLEAN DEFAULT 0, -- Review reminder flag
-- Dates (ISO 8601)
transaction_date TEXT NOT NULL, -- Date only: "2026-02-13"
created_at TEXT NOT NULL, -- Full datetime
updated_at TEXT NOT NULL,
-- Sync & Audit
version INTEGER DEFAULT 1,
device_id TEXT,
is_deleted BOOLEAN DEFAULT 0,
sync_status TEXT DEFAULT 'synced' CHECK(sync_status IN ('synced', 'pending', 'conflict', 'error')),
-- Constraints
CHECK (gross_amount GLOB '-?[0-9]*.?[0-9]*'),
CHECK (tax_amount GLOB '-?[0-9]*.?[0-9]*'),
CHECK (net_amount GLOB '-?[0-9]*.?[0-9]*'),
CHECK (tax_rate GLOB '-?[0-9]*.?[0-9]*' OR tax_rate IS NULL),
-- Enforce positive amounts for expenses
CHECK (
(transaction_type IN ('expense', 'transfer_out') AND CAST(gross_amount AS REAL) > 0)
OR
(transaction_type IN ('income', 'transfer_in') AND CAST(gross_amount AS REAL) > 0)
)
);
```
**Indexes:**
```sql
CREATE INDEX idx_transactions_account ON transactions(account_id, is_deleted);
CREATE INDEX idx_transactions_date ON transactions(transaction_date DESC);
CREATE INDEX idx_transactions_type ON transactions(transaction_type);
CREATE INDEX idx_transactions_schedule ON transactions(schedule_id);
CREATE INDEX idx_transactions_transfer ON transactions(transfer_id);
CREATE INDEX idx_transactions_review ON transactions(needs_review) WHERE needs_review = 1;
CREATE INDEX idx_transactions_auto ON transactions(is_auto_inserted) WHERE is_auto_inserted = 1;
```
### 4. transaction_tags
Many-to-many relationship between transactions and tags.
```sql
CREATE TABLE transaction_tags (
transaction_id TEXT NOT NULL REFERENCES transactions(id) ON DELETE CASCADE,
tag_id TEXT NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (transaction_id, tag_id)
);
```
**Indexes:**
```sql
CREATE INDEX idx_transaction_tags_tag ON transaction_tags(tag_id);
```
### 5. scheduled_transactions
Recurring transaction templates.
```sql
CREATE TABLE scheduled_transactions (
id TEXT PRIMARY KEY,
account_id TEXT NOT NULL REFERENCES accounts(id),
-- Schedule Pattern
schedule_type TEXT NOT NULL CHECK(schedule_type IN (
'daily',
'weekly', -- Select day(s) of week
'monthly', -- Select day of month
'yearly', -- Select month and day
'custom' -- Custom interval
)),
frequency INTEGER DEFAULT 1, -- Every N days/weeks/months
-- For weekly: 0=Sunday, 1=Monday, etc. (JSON array for multiple)
days_of_week JSON,
-- For monthly: 1-31 (-1 = last day of month)
day_of_month INTEGER,
-- For yearly: month (1-12)
month_of_year INTEGER,
-- Execution Time
execution_time TEXT DEFAULT '00:00', -- HH:MM format
timezone TEXT, -- User's timezone or explicit
-- Start/End Conditions
start_date TEXT NOT NULL,
end_date TEXT, -- NULL = indefinite
occurrence_count INTEGER, -- NULL = indefinite
current_occurrence INTEGER DEFAULT 0,
-- Transaction Template
transaction_type TEXT NOT NULL,
gross_amount TEXT NOT NULL,
tax_amount TEXT DEFAULT '0.00000000',
net_amount TEXT NOT NULL,
currency TEXT NOT NULL,
description TEXT,
merchant TEXT,
notes TEXT,
-- Tags (JSON array of tag IDs)
tag_ids JSON,
-- Status
is_active BOOLEAN DEFAULT 1,
last_generated_date TEXT,
next_execution_datetime TEXT, -- Full datetime in UTC
-- Sync & Audit
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
version INTEGER DEFAULT 1,
device_id TEXT,
is_deleted BOOLEAN DEFAULT 0,
-- Constraints
CHECK (CAST(gross_amount AS REAL) > 0),
CHECK (frequency > 0)
);
```
**Indexes:**
```sql
CREATE INDEX idx_scheduled_next_exec ON scheduled_transactions(next_execution_datetime)
WHERE is_active = 1 AND is_deleted = 0;
CREATE INDEX idx_scheduled_account ON scheduled_transactions(account_id);
```
### 6. scheduled_instances
Track generated instances of scheduled transactions.
```sql
CREATE TABLE scheduled_instances (
id TEXT PRIMARY KEY,
schedule_id TEXT NOT NULL REFERENCES scheduled_transactions(id) ON DELETE CASCADE,
transaction_id TEXT REFERENCES transactions(id),
due_date TEXT NOT NULL, -- Date this instance was due
is_generated BOOLEAN DEFAULT 0,
is_skipped BOOLEAN DEFAULT 0,
generated_at TEXT,
notified BOOLEAN DEFAULT 0, -- Track if user was reminded
created_at TEXT NOT NULL
);
```
**Indexes:**
```sql
CREATE INDEX idx_scheduled_instances_schedule ON scheduled_instances(schedule_id);
CREATE INDEX idx_scheduled_instances_due ON scheduled_instances(due_date)
WHERE is_generated = 0 AND is_skipped = 0;
```
### 7. goals
Financial goals with progress tracking.
```sql
CREATE TABLE goals (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
-- Target
target_amount TEXT NOT NULL,
current_amount TEXT NOT NULL DEFAULT '0.00000000',
currency TEXT NOT NULL,
-- Type
goal_type TEXT NOT NULL CHECK(goal_type IN (
'savings', -- Save up to target
'debt_payoff', -- Pay off debt
'spending_limit', -- Don't exceed limit
'custom'
)),
-- Timeline
target_date TEXT, -- Optional deadline
is_recurring BOOLEAN DEFAULT 0, -- Reset monthly/quarterly?
recurrence_period TEXT CHECK(recurrence_period IN ('monthly', 'quarterly', 'yearly')),
-- Link to account (optional)
linked_account_id TEXT REFERENCES accounts(id),
-- Visual
color TEXT,
icon TEXT,
-- Status
is_active BOOLEAN DEFAULT 1,
is_achieved BOOLEAN DEFAULT 0,
achieved_at TEXT,
last_reset_date TEXT, -- For recurring goals
-- Sync & Audit
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
version INTEGER DEFAULT 1,
device_id TEXT,
is_deleted BOOLEAN DEFAULT 0,
-- Constraints
CHECK (CAST(target_amount AS REAL) > 0),
CHECK (CAST(current_amount AS REAL) >= 0)
);
```
**Indexes:**
```sql
CREATE INDEX idx_goals_active ON goals(is_active, is_deleted);
CREATE INDEX idx_goals_achieved ON goals(is_achieved) WHERE is_achieved = 0;
CREATE INDEX idx_goals_account ON goals(linked_account_id);
```
### 8. goal_rules
Auto-contribution rules for goals.
```sql
CREATE TABLE goal_rules (
id TEXT PRIMARY KEY,
goal_id TEXT NOT NULL REFERENCES goals(id) ON DELETE CASCADE,
-- Match Conditions
-- Match if transaction has ALL specified tags (AND logic)
tag_ids JSON NOT NULL, -- Array of tag IDs
-- Contribution Calculation
contribution_type TEXT NOT NULL CHECK(contribution_type IN ('percentage', 'fixed')),
percentage TEXT, -- e.g., "10.00" = 10%
fixed_amount TEXT, -- Fixed amount per transaction
-- Limits
max_contribution_per_transaction TEXT, -- Cap per transaction
monthly_cap TEXT, -- Monthly contribution limit
is_active BOOLEAN DEFAULT 1,
-- Sync & Audit
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
version INTEGER DEFAULT 1,
device_id TEXT,
is_deleted BOOLEAN DEFAULT 0,
-- Constraints
CHECK (
(contribution_type = 'percentage' AND percentage IS NOT NULL)
OR
(contribution_type = 'fixed' AND fixed_amount IS NOT NULL)
),
CHECK (CAST(percentage AS REAL) >= 0 AND CAST(percentage AS REAL) <= 100)
);
```
**Indexes:**
```sql
CREATE INDEX idx_goal_rules_goal ON goal_rules(goal_id);
CREATE INDEX idx_goal_rules_active ON goal_rules(is_active);
```
### 9. goal_progress
Track contributions to goals.
```sql
CREATE TABLE goal_progress (
id TEXT PRIMARY KEY,
goal_id TEXT NOT NULL REFERENCES goals(id) ON DELETE CASCADE,
amount TEXT NOT NULL, -- Amount added (can be negative for adjustments)
transaction_id TEXT REFERENCES transactions(id),
notes TEXT,
recorded_at TEXT NOT NULL,
-- Constraints
CHECK (amount GLOB '-?[0-9]*.?[0-9]*')
);
```
**Indexes:**
```sql
CREATE INDEX idx_goal_progress_goal ON goal_progress(goal_id);
CREATE INDEX idx_goal_progress_date ON goal_progress(recorded_at);
```
### 10. transfers
Track account-to-account transfers with FX rates.
```sql
CREATE TABLE transfers (
id TEXT PRIMARY KEY,
from_account_id TEXT NOT NULL REFERENCES accounts(id),
to_account_id TEXT NOT NULL REFERENCES accounts(id),
from_transaction_id TEXT REFERENCES transactions(id),
to_transaction_id TEXT REFERENCES transactions(id),
-- Amounts
from_amount TEXT NOT NULL, -- In from_account currency
to_amount TEXT NOT NULL, -- In to_account currency
exchange_rate TEXT, -- Rate used (to_amount / from_amount)
exchange_rate_source TEXT, -- 'auto', 'manual', 'api'
fees TEXT DEFAULT '0.00000000',
description TEXT,
transfer_date TEXT NOT NULL,
created_at TEXT NOT NULL,
-- Sync & Audit
version INTEGER DEFAULT 1,
device_id TEXT,
is_deleted BOOLEAN DEFAULT 0,
-- Constraints
CHECK (from_account_id != to_account_id),
CHECK (CAST(from_amount AS REAL) > 0),
CHECK (CAST(to_amount AS REAL) > 0)
);
```
**Indexes:**
```sql
CREATE INDEX idx_transfers_from ON transfers(from_account_id);
CREATE INDEX idx_transfers_to ON transfers(to_account_id);
CREATE INDEX idx_transfers_date ON transfers(transfer_date);
```
### 11. exchange_rates
Historical exchange rates for accurate conversions.
```sql
CREATE TABLE exchange_rates (
from_currency TEXT NOT NULL,
to_currency TEXT NOT NULL,
rate TEXT NOT NULL, -- 8 decimal precision
date TEXT NOT NULL, -- Date only
source TEXT, -- 'ECB', 'HKMA', 'manual', etc.
fetched_at TEXT, -- When rate was obtained
PRIMARY KEY (from_currency, to_currency, date),
CHECK (from_currency != to_currency),
CHECK (CAST(rate AS REAL) > 0)
);
```
**Indexes:**
```sql
CREATE INDEX idx_exchange_rates_date ON exchange_rates(date);
CREATE INDEX idx_exchange_rates_lookup ON exchange_rates(from_currency, to_currency, date DESC);
```
### 12. reconciliations
Simple account reconciliation records.
```sql
CREATE TABLE reconciliations (
id TEXT PRIMARY KEY,
account_id TEXT NOT NULL REFERENCES accounts(id),
statement_date TEXT NOT NULL, -- Statement period end
statement_balance TEXT NOT NULL, -- Balance per statement
app_balance TEXT NOT NULL, -- Calculated balance
difference TEXT NOT NULL, -- statement - app
status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'balanced', 'adjusted')),
notes TEXT,
created_at TEXT NOT NULL,
resolved_at TEXT,
-- Sync & Audit
version INTEGER DEFAULT 1,
device_id TEXT,
CHECK (CAST(statement_balance AS REAL) IS NOT NULL),
CHECK (CAST(app_balance AS REAL) IS NOT NULL)
);
```
**Indexes:**
```sql
CREATE INDEX idx_reconciliations_account ON reconciliations(account_id);
CREATE INDEX idx_reconciliations_date ON reconciliations(statement_date DESC);
```
### 13. settings
Application settings key-value store.
```sql
CREATE TABLE settings (
key TEXT PRIMARY KEY,
value TEXT,
updated_at TEXT NOT NULL
);
```
**Default Settings:**
```sql
INSERT INTO settings (key, value, updated_at) VALUES
('language', 'en', datetime('now')),
('default_currency', 'HKD', datetime('now')),
('base_currency', 'HKD', datetime('now')),
('timezone', 'auto', datetime('now')),
('default_view', 'combined', datetime('now')), -- 'combined' or 'single'
('display_mode', 'net', datetime('now')), -- 'net' or 'gross'
('decimal_places', '2', datetime('now')), -- Display precision
('date_format', 'YYYY-MM-DD', datetime('now')),
('time_format', '24h', datetime('now')),
('theme', 'system', datetime('now')), -- 'light', 'dark', 'system'
('week_starts_on', '1', datetime('now')), -- 0=Sun, 1=Mon
('scheduled_check_interval', '1', datetime('now')); -- Minutes
```
## Database Initialization
```sql
-- Enable foreign keys
PRAGMA foreign_keys = ON;
-- Enable WAL mode for better concurrency
PRAGMA journal_mode = WAL;
-- Set synchronous mode for safety
PRAGMA synchronous = NORMAL;
-- Create all tables (in order)
-- ... (all CREATE TABLE statements above) ...
-- Insert default settings
-- ... (settings INSERT statements) ...
-- Insert default/system tags
INSERT INTO tags (id, name, color, icon, is_system, sort_order) VALUES
('tag-food', 'Food & Dining', '#EF4444', 'utensils', 1, 1),
('tag-transport', 'Transportation', '#3B82F6', 'car', 1, 2),
('tag-shopping', 'Shopping', '#8B5CF6', 'shopping-bag', 1, 3),
('tag-entertainment', 'Entertainment', '#F59E0B', 'film', 1, 4),
('tag-utilities', 'Utilities', '#10B981', 'zap', 1, 5),
('tag-health', 'Health & Medical', '#EC4899', 'heart-pulse', 1, 6),
('tag-education', 'Education', '#6366F1', 'graduation-cap', 1, 7),
('tag-salary', 'Salary', '#10B981', 'banknote', 1, 8),
('tag-investment', 'Investment', '#14B8A6', 'trending-up', 1, 9),
('tag-transfer', 'Transfer', '#6B7280', 'arrow-right-left', 1, 10);
```
## Common Queries
### Using SeaORM
#### Get Account Balance
```rust
use sea_orm::{entity::*, query::*};
use entities::account;
let balance: Option<String> = account::Entity::find_by_id(account_id)
.select_only()
.column(account::Column::CurrentBalance)
.into_tuple()
.one(&db)
.await?;
```
#### Get Transactions with Tags
```rust
use entities::{transaction, tag, transaction_tag};
let transactions = transaction::Entity::find()
.filter(transaction::Column::AccountId.eq(account_id))
.filter(transaction::Column::IsDeleted.eq(false))
.order_by_desc(transaction::Column::TransactionDate)
.find_with_linked(transaction::TagLink)
.all(&db)
.await?;
```
#### Get Spending by Tag (Monthly)
```rust
use sea_orm::{ sea_query::*, * };
let spending = transaction::Entity::find()
.select_only()
.column_as(tag::Column::Name, "tag_name")
.column_as(tag::Column::Color, "tag_color")
.column_as(transaction::Column::NetAmount.sum(), "total")
.inner_join(transaction_tag::Entity)
.inner_join(tag::Entity)
.filter(transaction::Column::TransactionType.eq("expense"))
.filter(transaction::Column::TransactionDate.gte("2026-02-01"))
.filter(transaction::Column::TransactionDate.lt("2026-03-01"))
.filter(transaction::Column::IsDeleted.eq(false))
.group_by(tag::Column::Id)
.order_by_desc(transaction::Column::NetAmount.sum())
.into_json()
.all(&db)
.await?;
```
#### Get Upcoming Scheduled Transactions
```rust
use entities::scheduled_transaction;
use chrono::Utc;
let now = Utc::now();
let upcoming = scheduled_transaction::Entity::find()
.filter(scheduled_transaction::Column::IsActive.eq(true))
.filter(scheduled_transaction::Column::IsDeleted.eq(false))
.filter(scheduled_transaction::Column::NextExecutionDatetime.lte(now))
.order_by_asc(scheduled_transaction::Column::NextExecutionDatetime)
.all(&db)
.await?;
```
#### Get Goal Progress
```rust
use entities::goal;
let goals = goal::Entity::find()
.filter(goal::Column::IsActive.eq(true))
.filter(goal::Column::IsDeleted.eq(false))
.order_by_desc(goal::Column::CreatedAt)
.all(&db)
.await?;
// Calculate progress in Rust
for goal in goals {
let current: f64 = goal.current_amount.parse()?;
let target: f64 = goal.target_amount.parse()?;
let progress_percent = (current / target) * 100.0;
}
```
### Raw SQL (if needed)
For complex queries, SeaORM supports raw SQL:
```rust
use sea_orm::{ConnectionTrait, Statement};
let stmt = Statement::from_sql_and_values(
db.get_database_backend(),
r#"
SELECT
tg.name,
tg.color,
SUM(CAST(t.net_amount AS REAL)) as total
FROM transactions t
JOIN transaction_tags tt ON t.id = tt.transaction_id
JOIN tags tg ON tt.tag_id = tg.id
WHERE t.transaction_type = 'expense'
AND strftime('%Y-%m', t.transaction_date) = ?
AND t.is_deleted = 0
GROUP BY tg.id
ORDER BY total DESC
"#,
["2026-02".into()]
);
let results = db.query_all(stmt).await?;
```
## Data Integrity with SeaORM
### Auto-Updating Timestamps and Version
With SeaORM, we handle these in the ActiveModel before updating:
```rust
// In your service/update function
pub async fn update_account(
db: &DatabaseConnection,
id: &str,
updates: AccountUpdate,
) -> Result<account::Model, DbErr> {
let account = account::Entity::find_by_id(id)
.one(db)
.await?
.ok_or(DbErr::Custom("Account not found".to_string()))?;
let mut active_model: account::ActiveModel = account.into();
// Update fields
if let Some(name) = updates.name {
active_model.name = Set(name);
}
// Auto-update audit fields
active_model.updated_at = Set(Utc::now());
active_model.version = Set(active_model.version.unwrap() + 1);
active_model.update(db).await
}
```
### Database Constraints
SeaORM supports check constraints in migrations:
```rust
// In migration file
.col(ColumnDef::new(Account::InitialBalance)
.string()
.not_null()
.check(Expr::col(Account::InitialBalance).like("-?[0-9]*.?[0-9]*")))
```
## Migration Strategy with SeaORM
SeaORM provides a built-in migration system:
### Migration Structure
```
src-tauri/src/migrations/
├── mod.rs
├── m001_initial.rs
├── m002_add_user_preferences.rs
└── m003_add_indexes.rs
```
### Migration Files
```rust
// src-tauri/src/migrations/mod.rs
pub use sea_orm_migration::prelude::*;
mod m001_initial;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m001_initial::Migration),
]
}
}
```
### Running Migrations
```rust
// In main.rs or lib.rs
use migrations::Migrator;
use sea_orm_migration::MigratorTrait;
pub async fn run_migrations(db: &DatabaseConnection) -> Result<(), DbErr> {
Migrator::up(db, None).await
}
```
### Migration Best Practices
1. **Each migration in separate file**: `m001_initial.rs`, `m002_...`
2. **Both up() and down()**: Always implement rollback
3. **Test migrations**: Test both up and down on sample data
4. **Never modify existing migrations** after deployment
5. **Always backup before migration**
6. **Handle data migration**: Use raw SQL for complex data transformations
### Checking Migration Status
```bash
# Check status
sea-orm-cli migrate status
# Run pending migrations
sea-orm-cli migrate up
# Rollback last migration
sea-orm-cli migrate down
# Rollback all migrations
sea-orm-cli migrate down -n 999
```
## Backup Format (JSON Export)
```json
{
"version": "1.0.0",
"exported_at": "2026-02-13T10:30:00Z",
"schema_version": 1,
"data": {
"accounts": [...],
"tags": [...],
"transactions": [...],
"transaction_tags": [...],
"scheduled_transactions": [...],
"scheduled_instances": [...],
"goals": [...],
"goal_rules": [...],
"goal_progress": [...],
"transfers": [...],
"exchange_rates": [...],
"reconciliations": [...],
"settings": [...]
},
"checksum": "sha256_hash"
}
```
## Performance Considerations
1. **Indexes**: All foreign keys and frequently queried columns indexed
2. **Pagination**: Use LIMIT/OFFSET for transaction lists
3. **Lazy loading**: Load receipts on demand
4. **Caching**: Cache account balances, don't calculate on every read
5. **Archiving**: Soft delete instead of hard delete for sync compatibility
## Security
1. **Encryption**: Optional SQLCipher for database encryption
2. **Key storage**: Use OS keychain (iOS Keychain, Android Keystore)
3. **Receipts**: Store locally, no cloud upload
4. **No network**: Except OCR API calls (opt-in)

935
doc/features.md Normal file
View File

@@ -0,0 +1,935 @@
# Feature Specifications
- [Feature Specifications](#feature-specifications)
- [1. Multi-Account Management](#1-multi-account-management)
- [Description](#description)
- [User Stories](#user-stories)
- [Features](#features)
- [Account Types](#account-types)
- [Account Properties](#account-properties)
- [Views](#views)
- [Acceptance Criteria](#acceptance-criteria)
- [2. Transaction Management](#2-transaction-management)
- [Description](#description-1)
- [User Stories](#user-stories-1)
- [Transaction Types](#transaction-types)
- [Expense](#expense)
- [Income](#income)
- [Transfer](#transfer)
- [Amount Handling](#amount-handling)
- [Tax Support](#tax-support)
- [Display Mode](#display-mode)
- [Transaction Properties](#transaction-properties)
- [Editing Historical Transactions](#editing-historical-transactions)
- [Acceptance Criteria](#acceptance-criteria-1)
- [3. Tag-Based Categorization](#3-tag-based-categorization)
- [Description](#description-2)
- [User Stories](#user-stories-2)
- [Tag Properties](#tag-properties)
- [Tag Assignment](#tag-assignment)
- [System Tags (Built-in)](#system-tags-built-in)
- [Budgeting by Tags](#budgeting-by-tags)
- [Acceptance Criteria](#acceptance-criteria-2)
- [4. Scheduled Transactions](#4-scheduled-transactions)
- [Description](#description-3)
- [User Stories](#user-stories-3)
- [Recurrence Patterns](#recurrence-patterns)
- [Daily](#daily)
- [Weekly](#weekly)
- [Monthly](#monthly)
- [Yearly](#yearly)
- [Custom](#custom)
- [Execution Configuration](#execution-configuration)
- [Time](#time)
- [End Conditions](#end-conditions)
- [Auto-Insertion Flow](#auto-insertion-flow)
- [User Review](#user-review)
- [Managing Instances](#managing-instances)
- [Acceptance Criteria](#acceptance-criteria-3)
- [5. Goal Tracking](#5-goal-tracking)
- [Description](#description-4)
- [User Stories](#user-stories-4)
- [Goal Types](#goal-types)
- [Savings](#savings)
- [Debt Payoff](#debt-payoff)
- [Spending Limit](#spending-limit)
- [Custom](#custom-1)
- [Goal Properties](#goal-properties)
- [Auto-Contribution Rules](#auto-contribution-rules)
- [Rule Configuration](#rule-configuration)
- [Examples](#examples)
- [Real-Time Processing](#real-time-processing)
- [Visual Progress](#visual-progress)
- [Goal Achievement](#goal-achievement)
- [Manual Contributions](#manual-contributions)
- [Acceptance Criteria](#acceptance-criteria-4)
- [6. Transfers Between Accounts](#6-transfers-between-accounts)
- [Description](#description-5)
- [User Stories](#user-stories-5)
- [Transfer Flow](#transfer-flow)
- [Paired Transactions](#paired-transactions)
- [Exchange Rate Handling](#exchange-rate-handling)
- [Auto-Fetch](#auto-fetch)
- [Manual Entry](#manual-entry)
- [Fees](#fees)
- [Same-Currency Transfers](#same-currency-transfers)
- [Acceptance Criteria](#acceptance-criteria-5)
- [7. Receipt Management](#7-receipt-management)
- [Description](#description-6)
- [User Stories](#user-stories-6)
- [Capture Methods](#capture-methods)
- [Storage](#storage)
- [OCR Processing](#ocr-processing)
- [OCR Correction UI](#ocr-correction-ui)
- [Multiple Receipts](#multiple-receipts)
- [Privacy \& Security](#privacy--security)
- [Acceptance Criteria](#acceptance-criteria-6)
- [8. Reconciliation](#8-reconciliation)
- [Description](#description-7)
- [User Stories](#user-stories-7)
- [Reconciliation Process](#reconciliation-process)
- [Simple Approach](#simple-approach)
- [Adjustment Transaction](#adjustment-transaction)
- [Reconciliation History](#reconciliation-history)
- [Acceptance Criteria](#acceptance-criteria-7)
- [9. Multi-Currency Support](#9-multi-currency-support)
- [Description](#description-8)
- [User Stories](#user-stories-8)
- [Currency Handling](#currency-handling)
- [Supported Currencies](#supported-currencies)
- [Base Currency](#base-currency)
- [Account Currency](#account-currency)
- [Exchange Rates](#exchange-rates)
- [Storage](#storage-1)
- [Sources](#sources)
- [Update Frequency](#update-frequency)
- [Conversion Logic](#conversion-logic)
- [Real-time Conversion](#real-time-conversion)
- [Historical Conversion](#historical-conversion)
- [Display Format](#display-format)
- [Acceptance Criteria](#acceptance-criteria-8)
- [10. Backup \& Export](#10-backup--export)
- [Description](#description-9)
- [User Stories](#user-stories-9)
- [Export](#export)
- [Import](#import)
- [Auto-Backup (Optional)](#auto-backup-optional)
- [Migration Support](#migration-support)
- [Acceptance Criteria](#acceptance-criteria-9)
- [11. Internationalization (i18n)](#11-internationalization-i18n)
- [Description](#description-10)
- [Supported Languages](#supported-languages)
- [Language Selection](#language-selection)
- [Translated Content](#translated-content)
- [RTL/LTR](#rtlltr)
- [Acceptance Criteria](#acceptance-criteria-10)
- [12. Settings \& Preferences](#12-settings--preferences)
- [Description](#description-11)
- [Settings Categories](#settings-categories)
- [General](#general)
- [Display](#display)
- [Appearance](#appearance)
- [Data](#data)
- [Scheduled Transactions](#scheduled-transactions)
- [About](#about)
- [Acceptance Criteria](#acceptance-criteria-11)
- [Future Features (Post-MVP)](#future-features-post-mvp)
- [Notifications](#notifications)
- [Cloud Sync](#cloud-sync)
- [Advanced Analytics](#advanced-analytics)
- [Investment Tracking](#investment-tracking)
- [Bill Management](#bill-management)
- [Bank Import](#bank-import)
- [Reports](#reports)
- [Shared Accounts](#shared-accounts)
## 1. Multi-Account Management
### Description
Users can create and manage multiple financial accounts (bank accounts, cash, digital wallets, loans) with individual currencies and balances.
### User Stories
- As a user, I want to add multiple accounts so I can track all my finances in one place
- As a user, I want to see a combined view of all accounts in my base currency
- As a user, I want to archive inactive accounts without losing history
- As a user, I want to customize account appearance with colors and icons
### Features
#### Account Types
1. **Checking** - Bank checking accounts
2. **Savings** - Bank savings accounts
3. **Credit Card** - Credit cards (negative balance = amount owed)
4. **Cash** - Physical cash tracking
5. **Digital Wallet** - PayMe, AlipayHK, WeChat Pay, Octopus, etc.
6. **Loan** - Loan/liability tracking
7. **Other** - Custom account types
#### Account Properties
- Name (customizable)
- Currency (ISO 4217)
- Initial balance
- Current balance (auto-calculated)
- Color (hex color picker)
- Icon (Lucide icons)
- Include in net worth (toggle)
- Show in combined view (toggle)
#### Views
1. **Combined View** (default): All accounts aggregated in base currency
2. **Single Account View**: Focus on one account with its transactions
### Acceptance Criteria
- [ ] User can create unlimited accounts
- [ ] Balance updates automatically when transactions added
- [ ] Combined view shows total net worth across selected accounts
- [ ] Archived accounts hidden by default but accessible
- [ ] Account deletion soft-deletes (preserves history)
---
## 2. Transaction Management
### Description
Full CRUD operations for expenses, incomes, and transfers with receipt support and precise financial tracking.
### User Stories
- As a user, I want to quickly add expenses with minimal steps
- As a user, I want to attach receipts to transactions for reference
- As a user, I want to edit historical transactions if I made a mistake
- As a user, I want to categorize transactions with multiple tags
### Transaction Types
#### Expense
- Money leaving an account
- Required fields: account, amount, description
- Optional: tags, merchant, notes, receipt, date
#### Income
- Money entering an account
- Same fields as expense
- Common examples: salary, refunds, gifts
#### Transfer
- Money moving between accounts
- Auto-creates paired transactions:
- Transfer Out (from account)
- Transfer In (to account)
- Supports different currencies with FX rate
- FX rate can be auto-fetched or manually entered
### Amount Handling
#### Tax Support
- **Gross Amount**: Pre-tax amount (true spending)
- **Tax Amount**: Tax portion (often 0 in Hong Kong)
- **Net Amount**: Post-tax (actual paid, matches bank statement)
- **Tax Rate**: Percentage stored with 8 decimal precision
#### Display Mode
- Default: Show **net amount** (what actually left account)
- Toggle: Show **gross amount** (for business expense tracking)
- Setting: Can be changed globally or per transaction
### Transaction Properties
- Account
- Type (expense/income/transfer)
- Amount (gross, tax, net)
- Currency
- Tags (multiple)
- Description
- Merchant
- Notes
- Receipt(s)
- Date
- Transfer linkage (if applicable)
### Editing Historical Transactions
- Allow editing any field
- Recalculate account balance
- Recalculate goal contributions
- Update reconciliation status (mark as needs review)
- Show edit history (optional future feature)
### Acceptance Criteria
- [ ] Add transaction in < 30 seconds
- [ ] Support multiple receipts per transaction
- [ ] Edit any historical transaction
- [ ] Delete with confirmation (soft delete)
- [ ] Search by description, merchant, amount, tags
- [ ] Filter by date range, account, type, tags
---
## 3. Tag-Based Categorization
### Description
Flat tag system allowing multiple tags per transaction for flexible categorization and budgeting.
### User Stories
- As a user, I want to categorize transactions with flexible tags
- As a user, I want to apply multiple tags to a single transaction
- As a user, I want to set budgets for specific tags
- As a user, I want to see spending breakdown by tags
### Tag Properties
- Name (unique)
- Color (full color picker)
- Icon (Lucide icon)
- Budget amount (optional)
- Budget period (daily/weekly/monthly/yearly)
### Tag Assignment
- Autocomplete input with tag creation
- Multiple tags per transaction (AND relationship)
- Quick-select recent tags
- Visual tag chips with color and icon
### System Tags (Built-in)
- Food & Dining
- Transportation
- Shopping
- Entertainment
- Utilities
- Health & Medical
- Education
- Salary
- Investment
- Transfer
### Budgeting by Tags
- Set budget for single tag
- Set budget for tag combination (e.g., "Food" + "Business")
- Track spending vs budget
- Alert at 80% and 100% of budget
### Acceptance Criteria
- [ ] Create custom tags with full color picker
- [ ] Apply multiple tags to one transaction
- [ ] Filter transactions by tag(s)
- [ ] View spending by tag over time
- [ ] Budget alerts when approaching limit
---
## 4. Scheduled Transactions
### Description
Recurring transaction templates that auto-generate on schedule with user review.
### User Stories
- As a user, I want to set up recurring bills (rent, utilities)
- As a user, I want to schedule monthly salary entries
- As a user, I want to review auto-generated transactions before they finalize
- As a user, I want to skip or modify individual instances
### Recurrence Patterns
#### Daily
- Every N days
- Example: Every 1 day, Every 7 days
#### Weekly
- Select day(s) of week (0=Sunday, 6=Saturday)
- Example: Every Monday and Friday
#### Monthly
- Select day of month (1-31)
- Special value: -1 = last day of month
- Example: 1st of every month, 15th of every month
#### Yearly
- Select month and day
- Example: January 1st (annual subscription)
#### Custom
- Every N days/weeks/months
- Full flexibility
### Execution Configuration
#### Time
- Configurable execution time per schedule (default 00:00)
- Stored in user's timezone
- Converted to UTC for storage
#### End Conditions
- **Never**: Continue indefinitely
- **After N occurrences**: Stop after specific count
- **Until date**: Stop on specific date
### Auto-Insertion Flow
1. Background check every minute
2. Find schedules where `next_execution_datetime <= now()`
3. Generate transaction:
- Copy template fields
- Set `is_auto_inserted = true`
- Set `needs_review = true`
- Set transaction date to execution date
4. Update schedule:
- Increment occurrence count
- Calculate next execution date
- Update `next_execution_datetime`
5. Show in "Needs Review" section
### User Review
- Dashboard badge with count
- List view of auto-inserted transactions
- Actions:
- **Confirm**: Remove `needs_review` flag
- **Edit**: Modify any field
- **Skip**: Mark instance as skipped (doesn't affect future)
- **Delete**: Remove transaction
### Managing Instances
- View upcoming instances (next 30 days)
- Skip individual instances
- Modify schedule (affects future instances only)
- Pause/resume schedule
### Acceptance Criteria
- [ ] Create schedules with all recurrence patterns
- [ ] Auto-insert on schedule with configurable time
- [ ] Mark auto-inserted as "needs review"
- [ ] Skip individual instances without affecting pattern
- [ ] Pause and resume schedules
- [ ] Edit schedule (future instances only)
---
## 5. Goal Tracking
### Description
Visual goal tracking with automatic contributions based on transaction rules.
### User Stories
- As a user, I want to save for a vacation with automatic contributions
- As a user, I want to track debt payoff progress
- As a user, I want to see visual progress toward my goals
- As a user, I want to celebrate when I achieve a goal
### Goal Types
#### Savings
- Save up to target amount
- Example: Vacation fund, emergency fund
#### Debt Payoff
- Pay off debt/loan
- Tracks reduction of liability
#### Spending Limit
- Don't exceed budget
- Tracks spending against limit
#### Custom
- User-defined goal type
### Goal Properties
- Name
- Description
- Target amount
- Current amount (auto-calculated)
- Currency
- Target date (optional)
- Recurring (reset monthly/quarterly/yearly)
- Linked account (optional)
- Color
- Icon
### Auto-Contribution Rules
#### Rule Configuration
- **Match Condition**: Transaction has ALL specified tags
- **Contribution Type**:
- **Percentage**: Contribute X% of transaction amount
- **Fixed**: Contribute $X per transaction
- **Limits**:
- Max per transaction
- Monthly cap
#### Examples
- "Contribute 10% of all Food transactions to Vacation Fund"
- "Contribute $5 from every Shopping transaction to Christmas Savings"
- "Contribute 50% of Salary to Emergency Fund"
#### Real-Time Processing
When transaction saved:
1. Check transaction tags
2. Find matching goal rules
3. Calculate contribution
4. Update goal.current_amount
5. Create goal_progress record
6. Check if goal achieved
7. Update UI immediately
8. Show toast notification
### Visual Progress
- Circular or linear progress bar
- Percentage complete
- Amount remaining
- Days until target date (if set)
- Recent contributions list
### Goal Achievement
- Mark as achieved when `current_amount >= target_amount`
- Show achievement date
- Option to reset (for recurring goals)
- Optional celebration animation
### Manual Contributions
- Add one-time contribution
- Adjust current amount
- Withdraw from goal (negative contribution)
### Acceptance Criteria
- [ ] Create goals with visual progress
- [ ] Set up auto-contribution rules by tags
- [ ] Real-time contribution when transaction added
- [ ] Toast notification on contribution
- [ ] Manual contribution/adjustment
- [ ] Achievement tracking with celebration
---
## 6. Transfers Between Accounts
### Description
Move money between accounts with automatic paired transaction creation and FX rate handling.
### User Stories
- As a user, I want to transfer between my accounts
- As a user, I want to handle different currencies in transfers
- As a user, I want to use custom exchange rates when needed
### Transfer Flow
1. User selects source account
2. User selects destination account
3. User enters amount in source currency
4. System shows estimated destination amount using current FX rate
5. User options:
- Accept estimated rate
- Enter custom rate
- Enter exact destination amount (reverse calculate rate)
6. Save creates paired transactions
### Paired Transactions
- **Transfer Out** (from account):
- Type: `transfer_out`
- Amount: source amount
- Currency: source account currency
- **Transfer In** (to account):
- Type: `transfer_in`
- Amount: destination amount
- Currency: destination account currency
- **Linking**:
- Both transactions share same `transfer_id`
- Cross-reference via `related_transaction_id`
### Exchange Rate Handling
#### Auto-Fetch
- Real-time rates from APIs:
- HKMA (for HKD rates)
- European Central Bank (for EUR rates)
- Fallback: XE.com or similar
- Cache rates for 1 hour
- Store historical rates
#### Manual Entry
- Override auto-fetched rate
- Enter custom rate
- Enter exact destination amount
- Record rate source as 'manual'
#### Fees
- Optional fee field
- Deducted from source or added to destination
- Track total cost of transfer
### Same-Currency Transfers
- Simplified flow (no FX rate needed)
- Amount is identical in both accounts
- Just creates paired transactions
### Acceptance Criteria
- [ ] Transfer between any two accounts
- [ ] Support different currencies
- [ ] Auto-fetch exchange rates
- [ ] Allow manual rate override
- [ ] Show transfer history
- [ ] Edit transfer updates both transactions
---
## 7. Receipt Management
### Description
Capture, store, and process receipts with OCR for automatic data extraction.
### User Stories
- As a user, I want to take photos of receipts for record keeping
- As a user, I want OCR to extract receipt data automatically
- As a user, I want to review and correct OCR results
- As a user, I want to view receipts later for reference
### Capture Methods
1. **Camera**: Native camera access (Tauri)
2. **File Picker**: Select from gallery/files
3. **Drag & Drop**: Desktop web (if applicable)
### Storage
- **Location**: Local filesystem only
- **Path**: `{app_data}/receipts/{account_id}/{year}/{month}/{transaction_id}_{timestamp}.{ext}`
- **Database**: Store relative paths in `receipt_paths` JSON array
- **Retention**: Keep forever (no auto-delete)
- **Security**: Optional encryption at rest
### OCR Processing
- **Service**: Google Vision API (cloud-based, requires internet)
- **Process**:
1. Capture/Upload image
2. Compress/optimize image
3. Send to Vision API
4. Extract structured data:
- Date
- Merchant
- Total amount
- Items (if available)
5. Store raw OCR text
6. Create transaction with extracted data
7. Mark as "needs review"
### OCR Correction UI
- Show extracted fields
- Allow editing before saving
- Confidence scores (if available)
- Manual entry fallback
### Multiple Receipts
- Support multiple images per transaction
- Gallery view
- Swipe between receipts
- Delete individual receipts
### Privacy & Security
- **Local only**: No cloud storage of receipts
- **Encryption**: Optional SQLCipher for database
- **Offline**: OCR requires internet, but storage is local
### Acceptance Criteria
- [ ] Capture receipt via camera
- [ ] Upload from file system
- [ ] OCR extracts date, merchant, amount
- [ ] Review and correct OCR results
- [ ] Multiple receipts per transaction
- [ ] View receipts in transaction detail
- [ ] Delete receipts (permanent)
---
## 8. Reconciliation
### Description
Simple account reconciliation to verify app balance matches bank statements.
### User Stories
- As a user, I want to reconcile my accounts with bank statements
- As a user, I want to know if my records match the bank
- As a user, I want to adjust for discrepancies
### Reconciliation Process
1. Select account to reconcile
2. Enter statement date (period end)
3. Enter statement ending balance
4. System calculates app balance as of that date
5. Shows difference (should be 0)
6. Options:
- **Balanced**: Difference is 0
- **Adjust**: Create adjustment transaction
- **Review**: List unreconciled transactions
### Simple Approach
- Single reconciliation record per statement
- No line-by-line matching
- Full trust in calculated balance
- Adjustment transaction reconciles difference
### Adjustment Transaction
- Special transaction type: `adjustment`
- Description: "Reconciliation adjustment {date}"
- Amount: Difference (positive or negative)
- Auto-tags: `reconciliation`
- Updates account balance
### Reconciliation History
- List all reconciliations for account
- Show date, statement balance, difference, status
- Filter by status
### Acceptance Criteria
- [ ] Reconcile any account
- [ ] Enter statement date and balance
- [ ] Calculate difference automatically
- [ ] Create adjustment transaction if needed
- [ ] View reconciliation history
- [ ] Mark transactions as affecting reconciliation
---
## 9. Multi-Currency Support
### Description
Full multi-currency support with exchange rate tracking and conversion.
### User Stories
- As a user, I want to track accounts in different currencies
- As a user, I want to see my total net worth in my base currency
- As a user, I want accurate historical exchange rates for reports
### Currency Handling
#### Supported Currencies
- **Fiat**: HKD, USD, CNY, EUR, GBP, JPY, and others
- **Crypto**: BTC, ETH (8 decimal support)
- **Custom**: User-defined currencies
#### Base Currency
- User sets one base currency
- Combined view converts all to base currency
- Reports use base currency
#### Account Currency
- Each account has its own currency
- Transactions stored in account's currency
- Display shows both: "HK$ 100.00 (≈ $12.80)"
### Exchange Rates
#### Storage
- Daily rates stored in `exchange_rates` table
- Historical rates preserved
- 8 decimal precision
#### Sources
1. **Auto-fetch**:
- HKMA (Hong Kong Monetary Authority) for HKD
- European Central Bank for EUR
- Fallback APIs (XE.com, etc.)
2. **Manual entry**:
- User enters custom rate
- Used for specific transactions
- Marked as source: 'manual'
#### Update Frequency
- Daily auto-fetch (background)
- Manual refresh option
- Store fetch timestamp
### Conversion Logic
#### Real-time Conversion
- Use most recent rate for current conversions
- Show rate date in UI
#### Historical Conversion
- Use rate from transaction date
- Accurate for historical reports
#### Display Format
```
Amount: HK$ 1,234.56789012
Rate: 7.84950000 (2026-02-13)
Converted: $157.28000000
```
### Acceptance Criteria
- [ ] Multiple currencies per user
- [ ] Set base currency for combined view
- [ ] Auto-fetch daily exchange rates
- [ ] Manual rate entry for transactions
- [ ] Historical rate tracking
- [ ] Display converted amounts in real-time
---
## 10. Backup & Export
### Description
JSON-based backup and restore functionality.
### User Stories
- As a user, I want to backup my data to a file
- As a user, I want to restore from a backup
- As a user, I want to transfer data to a new device
### Export
- **Format**: JSON with checksum
- **Content**: All tables (accounts, transactions, goals, etc.)
- **Compression**: Optional ZIP
- **Location**: User-selected folder
- **Encryption**: Optional password protection
### Import
- **Validation**: Verify checksum and schema version
- **Merge or Replace**:
- Replace: Clear database, import all
- Merge: Import new records, update existing by ID
- **Conflict Resolution**: Last-write-wins or manual merge
- **Progress**: Show import progress for large datasets
### Auto-Backup (Optional)
- Periodic automatic export
- User selects folder and frequency
- Keep last N backups
### Migration Support
- Schema version check
- Automatic migration if possible
- Manual guidance if incompatible
### Acceptance Criteria
- [ ] Export all data to JSON
- [ ] Import from JSON file
- [ ] Verify data integrity with checksum
- [ ] Optional password encryption
- [ ] Show export/import progress
- [ ] Handle schema version differences
---
## 11. Internationalization (i18n)
### Description
Multi-language support with English and Traditional Chinese.
### Supported Languages
- **English** (en) - Primary
- **Traditional Chinese** (zh-Hant) - 繁體中文
### Language Selection
- Auto-detect from system
- Manual override in settings
- Persist selection
### Translated Content
- All UI labels and buttons
- Error messages
- Date/time formats
- Currency display formats
- Category/tag names (user-defined tags in user's language)
### RTL/LTR
- Both languages LTR (no RTL support needed)
### Acceptance Criteria
- [ ] Switch language in settings
- [ ] All UI text translated
- [ ] Proper date/time localization
- [ ] Currency format per locale
- [ ] Persist language preference
---
## 12. Settings & Preferences
### Description
User-configurable settings for personalization.
### Settings Categories
#### General
- Language (en/zh-Hant)
- Default currency
- Base currency for combined view
- Timezone (auto/manual)
#### Display
- Default view (combined/single)
- Display mode (net/gross amounts)
- Decimal places (2-8)
- Date format (YYYY-MM-DD, DD/MM/YYYY, etc.)
- Time format (12h/24h)
- Week starts on (Sunday/Monday)
#### Appearance
- Theme (light/dark/system)
- Accent color
- Font size
#### Data
- Auto-backup toggle
- Backup frequency
- Backup location
- Export data
- Import data
#### Scheduled Transactions
- Check interval (minutes)
- Default execution time
- Notification preferences (future)
#### About
- App version
- Build number
- Open source licenses
- Privacy policy
- Terms of use
### Acceptance Criteria
- [ ] All settings persist across sessions
- [ ] Immediate UI updates on setting change
- [ ] Reset to defaults option
- [ ] Import/export settings (optional)
---
## Future Features (Post-MVP)
### Notifications
- Scheduled transaction reminders
- Budget alerts
- Goal achievement
- Bill due dates
### Cloud Sync
- Multi-device synchronization
- Conflict resolution UI
- End-to-end encryption
### Advanced Analytics
- Spending trends
- Cash flow forecasting
- Net worth over time
- Category trends
### Investment Tracking
- Stock portfolios
- Real-time prices
- Gain/loss calculations
### Bill Management
- Bill reminders
- Recurring bill tracking
- Payment history
### Bank Import
- HSBC Open Banking API
- Hang Seng API
- OFX/QFX file import
- CSV import with mapping
### Reports
- PDF export
- Tax reports
- Custom date ranges
- Scheduled reports
### Shared Accounts
- Family/partner sharing
- Permission levels
- Activity log

1357
doc/technical-specs.md Normal file

File diff suppressed because it is too large Load Diff

852
doc/ui-design.md Normal file
View File

@@ -0,0 +1,852 @@
# UI/UX Design Guidelines
## Design Philosophy
### Mobile-First Approach
- Touch-optimized interface (minimum 44px touch targets)
- Thumb-friendly navigation (bottom bar)
- Swipe gestures for quick actions
- Pull-to-refresh patterns
- Full-screen modals instead of popups
### Visual Language
- Clean, minimal design
- Consistent spacing and typography
- Clear visual hierarchy
- Accessible color contrast
- Light/Dark mode support
### Performance
- Fast load times (< 3 seconds)
- Smooth animations (60fps)
- Optimized lists (virtualization)
- Lazy loading for images
## Color System
### Primary Colors
```css
--primary-50: #eff6ff;
--primary-100: #dbeafe;
--primary-200: #bfdbfe;
--primary-300: #93c5fd;
--primary-400: #60a5fa;
--primary-500: #3b82f6; /* Main primary */
--primary-600: #2563eb;
--primary-700: #1d4ed8;
--primary-800: #1e40af;
--primary-900: #1e3a8a;
```
### Semantic Colors
```css
/* Success */
--success-500: #22c55e;
--success-bg: #dcfce7;
/* Warning */
--warning-500: #f59e0b;
--warning-bg: #fef3c7;
/* Error */
--error-500: #ef4444;
--error-bg: #fee2e2;
/* Info */
--info-500: #3b82f6;
--info-bg: #dbeafe;
```
### Account Type Colors
```css
--checking: #3b82f6; /* Blue */
--savings: #10b981; /* Green */
--credit-card: #ef4444; /* Red */
--cash: #f59e0b; /* Amber */
--digital-wallet: #8b5cf6; /* Purple */
--loan: #6b7280; /* Gray */
```
### Transaction Type Colors
```css
--expense: #ef4444; /* Red */
--income: #10b981; /* Green */
--transfer: #3b82f6; /* Blue */
```
## Typography
### Font Stack
```css
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
```
### Type Scale
```css
/* Heading */
--text-4xl: 2.25rem; /* 36px - Page title */
--text-3xl: 1.875rem; /* 30px - Section title */
--text-2xl: 1.5rem; /* 24px - Card title */
--text-xl: 1.25rem; /* 20px - Subsection */
/* Body */
--text-lg: 1.125rem; /* 18px - Large text */
--text-base: 1rem; /* 16px - Body */
--text-sm: 0.875rem; /* 14px - Small text */
--text-xs: 0.75rem; /* 12px - Labels */
```
### Font Weights
```css
--font-normal: 400;
--font-medium: 500;
--font-semibold: 600;
--font-bold: 700;
```
## Spacing System
```css
--space-1: 0.25rem; /* 4px */
--space-2: 0.5rem; /* 8px */
--space-3: 0.75rem; /* 12px */
--space-4: 1rem; /* 16px */
--space-5: 1.25rem; /* 20px */
--space-6: 1.5rem; /* 24px */
--space-8: 2rem; /* 32px */
--space-10: 2.5rem; /* 40px */
--space-12: 3rem; /* 48px */
```
## Layout
### Screen Sizes
```css
/* Mobile (default) */
--screen-sm: 640px;
/* Tablet */
--screen-md: 768px;
/* Desktop */
--screen-lg: 1024px;
/* Large Desktop */
--screen-xl: 1280px;
```
### Safe Areas (iOS)
```css
/* Account for notch and home indicator */
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
```
### Container
```css
/* Mobile */
max-width: 100%;
padding: 1rem;
/* Tablet+ */
@media (min-width: 768px) {
max-width: 768px;
margin: 0 auto;
}
```
## Navigation
### Bottom Navigation (Mobile)
```
┌─────────────────────────────────────┐
│ [Content] │
│ │
├─────────┬─────────┬─────────┬───────┤
│ Home │Accounts │ Add │ Goals │
│ (icon) │ (icon) │ (FAB) │(icon) │
└─────────┴─────────┴─────────┴───────┘
```
- Fixed at bottom
- Icons with labels
- Center FAB for quick add
- 5 items max
### Side Navigation (Desktop/Tablet)
```
┌──────────┬──────────────────────────┐
│ │ │
│ Logo │ │
│ │ [Content] │
│ Home │ │
│ Accounts│ │
│ Goals │ │
│ Settings│ │
│ │ │
└──────────┴──────────────────────────┘
```
### Navigation Items
1. **Home** - Dashboard
2. **Accounts** - Account list and details
3. **Add** - Quick add transaction (FAB)
4. **Goals** - Goal tracking
5. **Settings** - App settings
## Screens
### 1. Dashboard (Home)
**Layout:**
```
┌─────────────────────────────┐
│ Net Worth: $125,000.00 │
│ ↑ 12% from last month │
├─────────────────────────────┤
│ ⚠️ Needs Review (3) │
├─────────────────────────────┤
│ Upcoming (next 7 days) │
│ • Rent - $15,000 (2 days) │
│ • Salary +$50,000 (5 days) │
├─────────────────────────────┤
│ Recent Transactions │
│ [List of 10 items] │
├─────────────────────────────┤
│ Goals Progress │
│ [3-4 goal cards] │
└─────────────────────────────┘
```
**Components:**
- Net worth card with trend
- "Needs Review" alert card (if any)
- Upcoming scheduled transactions
- Recent transactions list (scrollable)
- Goal progress cards (horizontal scroll)
### 2. Accounts List
**Layout:**
```
┌─────────────────────────────┐
│ Accounts [+] │
├─────────────────────────────┤
│ Combined View [Toggle] │
├─────────────────────────────┤
│ 💳 Checking HKD $50k │
│ 💰 Savings HKD $100k │
│ 💵 Cash USD $5k │
│ 📱 PayMe HKD $2k │
│ 💳 Credit Card -$3k │
└─────────────────────────────┘
```
**Components:**
- Account cards with icon, name, balance
- Color-coded by type
- Tap to view account details
- Swipe to edit/delete
- FAB to add new account
### 3. Account Detail
**Layout:**
```
┌─────────────────────────────┐
│ ← Checking Account │
│ $50,000.00 HKD │
│ [Graph: Balance over time] │
├─────────────────────────────┤
│ This Month: -$5,230 │
├─────────────────────────────┤
│ Transactions │
│ [Filter Bar] │
│ • [Search] [Date] [Tags] │
├─────────────────────────────┤
│ Feb 13 Grocery -$230 │
│ Feb 12 Salary +$30k │
│ Feb 11 Transfer -$10k │
│ ... │
└─────────────────────────────┘
```
**Components:**
- Account header with balance
- Mini chart (optional)
- Monthly summary
- Filterable transaction list
- Date grouping
### 4. Transaction List
**Layout:**
```
┌─────────────────────────────┐
│ Transactions [Filter] │
├─────────────────────────────┤
│ [Search...] [Sort] │
├─────────────────────────────┤
│ Today │
│ 🍔 Lunch -$120 Food │
│ 🚇 MTR -$50 Trans │
├─────────────────────────────┤
│ Yesterday │
│ 🛒 Supermarket -$450 Shop │
│ 💰 Salary +$50k Salary│
├─────────────────────────────┤
│ Feb 11 │
│ ... │
└─────────────────────────────┘
```
**Components:**
- Search bar
- Filter chips (account, type, tags, date range)
- Sort dropdown (date, amount)
- Date-grouped list
- Swipe actions (edit/delete)
- Pull-to-refresh
### 5. Add Transaction
**Layout (Step by Step or Single Form):**
```
┌─────────────────────────────┐
│ Add Transaction [Save] │
├─────────────────────────────┤
│ [Expense] [Income] [Trans] │
├─────────────────────────────┤
│ Amount: │
│ [HKD $ 0.00 ] │
├─────────────────────────────┤
│ Tax: [None] [Manual] │
│ Gross: [ 0.00] │
│ Tax: [ 0.00] │
│ Net: [ 0.00] │
├─────────────────────────────┤
│ Account: │
│ [▼ Checking (HKD) ] │
├─────────────────────────────┤
│ Tags: │
│ [Food 🍔] [Dining 🍽️] [+Add]│
├─────────────────────────────┤
│ Description: │
│ [Lunch at restaurant ] │
├─────────────────────────────┤
│ Merchant: │
│ [McDonald's ] │
├─────────────────────────────┤
│ Date: [Feb 13, 2026] │
├─────────────────────────────┤
│ Make Recurring: [ ] │
│ (shows schedule options) │
├─────────────────────────────┤
│ Attach Receipt: │
│ [📷 Camera] [📁 Files] │
└─────────────────────────────┘
```
**Components:**
- Transaction type tabs
- Currency-aware amount input
- Tax toggle (show/hide)
- Account selector
- Tag input (autocomplete, multi-select)
- Date picker
- Scheduled transaction toggle
- Receipt attachment
### 6. Transfer Form
**Layout:**
```
┌─────────────────────────────┐
│ Transfer [Save] │
├─────────────────────────────┤
│ From: │
│ [▼ Checking (HKD) ] │
├─────────────────────────────┤
│ To: │
│ [▼ Savings (USD) ] │
├─────────────────────────────┤
│ Amount: │
│ From: [HKD $10,000.00] │
├─────────────────────────────┤
│ Exchange Rate: │
│ [Auto 0.1280] [Manual] │
│ To: [USD $1,280.00] │
├─────────────────────────────┤
│ Fee: [HKD $0.00] │
├─────────────────────────────┤
│ Description: │
│ [Monthly savings transfer] │
├─────────────────────────────┤
│ Date: [Feb 13, 2026] │
└─────────────────────────────┘
```
**Components:**
- From/To account selectors
- Amount input
- Exchange rate display/toggle
- Fee input
- Description
- Date picker
### 7. Goals
**Layout:**
```
┌─────────────────────────────┐
│ Goals [+] │
├─────────────────────────────┤
│ Active Goals (3) │
├─────────────────────────────┤
│ ┌───────────────────────┐ │
│ │ 🏖️ Vacation │ │
│ │ [████████░░░░] 75% │ │
│ │ $15k / $20k HKD │ │
│ │ 30 days remaining │ │
│ └───────────────────────┘ │
├─────────────────────────────┤
│ ┌───────────────────────┐ │
│ │ 🚗 New Car │ │
│ │ [████░░░░░░░░] 35% │ │
│ │ $70k / $200k HKD │ │
│ └───────────────────────┘ │
├─────────────────────────────┤
│ Achieved Goals (2) [v] │
│ [Expandable section] │
└─────────────────────────────┘
```
**Components:**
- Goal cards with progress bar
- Percentage complete
- Amount remaining
- Days remaining (if target date set)
- Quick-add contribution button
- Tap to view goal details
### 8. Goal Detail
**Layout:**
```
┌─────────────────────────────┐
│ ← Vacation Fund │
│ [Circular Progress: 75%] │
│ $15,000 / $20,000 HKD │
│ Target: June 2026 │
├─────────────────────────────┤
│ Auto-Contribution Rules: │
│ • 10% of Food transactions │
│ • $5 from Shopping │
│ [Edit Rules] │
├─────────────────────────────┤
│ Recent Contributions: │
│ • Today +$12 Lunch │
│ • Yesterday +$45 Dinner │
│ • Feb 11 +$5 Shopping│
├─────────────────────────────┤
│ [Add Contribution] │
└─────────────────────────────┘
```
**Components:**
- Large progress indicator
- Contribution rules list
- Recent contributions
- Manual add button
- Edit goal button
### 9. Scheduled Transactions
**Layout:**
```
┌─────────────────────────────┐
│ Scheduled [+] │
├─────────────────────────────┤
│ Active (5) │
│ • Rent - Monthly - $15k │
│ • Salary - Monthly +$50k │
│ • Netflix - Monthly -$93 │
├─────────────────────────────┤
│ Upcoming (7 days) │
│ • Gym Fee - Feb 15 │
│ • Phone Bill - Feb 16 │
├─────────────────────────────┤
│ ⚠️ Needs Review (3) │
│ • Auto: Electricity Bill │
│ • Auto: Internet Bill │
└─────────────────────────────┘
```
**Components:**
- Active schedules list
- Upcoming instances (next 7 days)
- "Needs Review" section
- Tap to edit schedule
### 10. Settings
**Layout:**
```
┌─────────────────────────────┐
│ Settings │
├─────────────────────────────┤
│ GENERAL │
│ Language: English ► │
│ Base Currency: HKD ► │
│ Timezone: Auto ► │
├─────────────────────────────┤
│ DISPLAY │
│ Theme: System ► │
│ Date Format: YYYY-MM-DD ► │
│ Decimal Places: 2 ► │
├─────────────────────────────┤
│ DATA │
│ Export Data ► │
│ Import Data ► │
│ Backup Location ► │
├─────────────────────────────┤
│ ABOUT │
│ Version 1.0.0 │
│ Privacy Policy ► │
│ Terms of Use ► │
└─────────────────────────────┘
```
**Components:**
- Grouped settings
- Navigation to detail pages
- Toggles for booleans
- Pickers for selections
## Components
### Buttons
**Primary Button:**
- Background: `--primary-500`
- Text: white
- Padding: 12px 24px
- Border-radius: 8px
- Full width on mobile
**Secondary Button:**
- Background: transparent
- Border: 1px solid `--primary-500`
- Text: `--primary-500`
**Danger Button:**
- Background: `--error-500`
- Text: white
**FAB (Floating Action Button):**
- Size: 56px
- Background: `--primary-500`
- Icon: white
- Shadow: medium
- Position: fixed bottom center
### Cards
**Account Card:**
```
┌─────────────────────────────┐
│ 💳 [Icon] │
│ Checking Account │
│ $50,000.00 HKD │
└─────────────────────────────┘
```
- Border-radius: 12px
- Shadow: light
- Padding: 16px
- Left border: 4px account color
**Transaction Card:**
```
┌─────────────────────────────┐
│ 🍔 Lunch at McDonald's │
│ Today -$120.00 [Food] │
└─────────────────────────────┘
```
- No border
- Bottom border: 1px `--gray-200`
- Padding: 12px 16px
**Goal Card:**
```
┌─────────────────────────────┐
│ 🏖️ Vacation Fund │
│ [Progress Bar: 75%] │
│ $15,000 / $20,000 │
└─────────────────────────────┘
```
- Border-radius: 12px
- Background: subtle gradient
- Progress bar: rounded
### Forms
**Input Field:**
- Height: 48px
- Border: 1px solid `--gray-300`
- Border-radius: 8px
- Padding: 0 12px
- Focus: border `--primary-500`
**Label:**
- Font-size: 14px
- Color: `--gray-600`
- Margin-bottom: 4px
**Error State:**
- Border: `--error-500`
- Error text below: 12px, `--error-500`
### Lists
**Transaction List:**
- Group by date
- Date header: sticky
- Item: icon + description + amount + tags
- Swipe: edit/delete actions
**Account List:**
- Card-based
- Tap to open
- Long-press: context menu
### Modals
**Bottom Sheet (Mobile):**
- Slides up from bottom
- 90% height
- Close on backdrop tap
- Drag to dismiss
**Full Screen:**
- For complex forms
- Header with back/cancel
- Scrollable content
**Dialog:**
- Centered
- Max-width: 400px
- For confirmations
### Icons
**Library:** Lucide React
**Size:**
- Small: 16px
- Default: 20px
- Large: 24px
- XL: 32px
**Color:**
- Default: `--gray-600`
- Active: `--primary-500`
- Error: `--error-500`
- Success: `--success-500`
## Interactions
### Touch Gestures
**Swipe Left (Transaction):**
- Reveal: Delete action
- Color: `--error-500`
**Swipe Right (Transaction):**
- Reveal: Edit action
- Color: `--primary-500`
**Pull Down:**
- Refresh list
- Loading spinner
**Long Press:**
- Context menu
- Multi-select mode
### Animations
**Page Transitions:**
- Duration: 300ms
- Easing: ease-in-out
- Direction: slide left/right
**List Items:**
- Stagger: 50ms delay
- Fade + slide up
**Modals:**
- Backdrop fade: 200ms
- Content slide: 300ms
**Progress Bars:**
- Animated fill
- Duration: 1s
- Easing: ease-out
### Feedback
**Toast Notifications:**
- Position: top center
- Duration: 3 seconds
- Types: success, error, info
**Success:**
- Checkmark icon
- Green background
- "Transaction saved"
**Error:**
- Alert icon
- Red background
- "Failed to save"
**Info:**
- Info icon
- Blue background
- "Added $50 to goal"
**Loading:**
- Spinner overlay
- Disabled interactions
- Progress for long operations
## Responsive Breakpoints
```css
/* Mobile (default) */
/* 0 - 639px */
- Bottom navigation
- Full-width cards
- Single column
- Touch gestures
/* Tablet */
/* 640px - 1023px */
- Side navigation (collapsible)
- 2-column layout for lists
- Larger touch targets
- Hover states
/* Desktop */
/* 1024px+ */
- Fixed side navigation
- Multi-column dashboard
- Compact density option
- Keyboard shortcuts (future)
```
## Accessibility
### Color Contrast
- Minimum 4.5:1 for text
- Minimum 3:1 for UI components
### Touch Targets
- Minimum 44x44px
- Adequate spacing between targets
### Screen Readers
- Semantic HTML
- ARIA labels
- Focus management
### Motion
- Respect `prefers-reduced-motion`
- Provide static alternatives
## Dark Mode
### Color Mapping
```css
/* Background */
--bg-primary: #0f172a; /* Slate 900 */
--bg-secondary: #1e293b; /* Slate 800 */
--bg-tertiary: #334155; /* Slate 700 */
/* Text */
--text-primary: #f8fafc; /* Slate 50 */
--text-secondary: #cbd5e1; /* Slate 300 */
--text-tertiary: #94a3b8; /* Slate 400 */
/* Border */
--border: #334155; /* Slate 700 */
```
### Components in Dark Mode
- Cards: `--bg-secondary`
- Inputs: `--bg-tertiary`
- Text: `--text-primary`
- Reduced opacity for images
## Error States
### Empty States
```
┌─────────────────────────────┐
│ [Illustration] │
│ │
│ No transactions yet │
│ │
│ Tap + to add your first │
│ transaction │
│ │
└─────────────────────────────┘
```
### Error Pages
```
┌─────────────────────────────┐
│ [Error Icon] │
│ │
│ Something went wrong │
│ │
│ [Retry] │
│ │
└─────────────────────────────┘
```
### Loading States
- Skeleton screens for lists
- Spinners for buttons
- Progress bars for uploads
## Onboarding
### First Launch
1. **Welcome Screen**: App value proposition
2. **Currency Setup**: Select base currency
3. **First Account**: Create initial account
4. **First Transaction**: Quick tutorial
### Empty Data
- Helpful illustrations
- Call-to-action buttons
- Tooltips on first use
## Design Assets
### Required Assets
- App icon (iOS/Android)
- Splash screen
- Empty state illustrations
- Achievement badges (optional)
### Icon Sizes
- iOS: 1024x1024, 180x180, 120x120, etc.
- Android: 512x512, 192x192, 144x144, etc.
### Splash Screen
- Logo centered
- Brand color background
- Duration: 2 seconds max

469
doc/validation.md Normal file
View File

@@ -0,0 +1,469 @@
# Validation
## Overview
Input validation is handled on both frontend (TypeScript/Zod) and backend (Rust/validator) to ensure data integrity and security.
## Backend Validation (Rust)
### Validator Crate
We use the `validator` crate with derive macros for declarative validation:
```toml
[dependencies]
validator = { version = "0.16", features = ["derive"] }
```
### DTO Validation
All input data is validated using DTOs (Data Transfer Objects) before processing:
```rust
// src-tauri/src/dto/account_dto.rs
use validator::{Validate, ValidationError};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Validate)]
pub struct CreateAccountDto {
#[validate(length(min = 1, max = 100, message = "Name must be between 1 and 100 characters"))]
pub name: String,
#[validate(custom(function = "validate_account_type"))]
pub account_type: String,
#[validate(length(equal = 3, message = "Currency must be 3-letter ISO code"))]
pub currency: String,
#[validate(custom(function = "validate_decimal_string"))]
pub initial_balance: String,
#[validate(regex(path = "*HEX_COLOR_REGEX", message = "Color must be valid hex code"))]
pub color: Option<String>,
}
fn validate_account_type(account_type: &str) -> Result<(), ValidationError> {
let valid_types = ["checking", "savings", "credit_card", "cash", "digital_wallet", "loan", "other"];
if valid_types.contains(&account_type) {
Ok(())
} else {
Err(ValidationError::new("invalid_account_type"))
}
}
fn validate_decimal_string(amount: &str) -> Result<(), ValidationError> {
// Validate 8 decimal precision format
if let Ok(dec) = rust_decimal::Decimal::from_str(amount) {
if dec.scale() <= 8 {
Ok(())
} else {
Err(ValidationError::new("too_many_decimals"))
}
} else {
Err(ValidationError::new("invalid_decimal"))
}
}
lazy_static::lazy_static! {
static ref HEX_COLOR_REGEX: regex::Regex = regex::Regex::new(r"^#[0-9A-Fa-f]{6}$").unwrap();
}
```
### Transaction DTO
```rust
// src-tauri/src/dto/transaction_dto.rs
use validator::{Validate, ValidationError};
use serde::{Deserialize, Serialize};
use rust_decimal::Decimal;
#[derive(Debug, Serialize, Deserialize, Validate)]
pub struct CreateTransactionDto {
#[validate(uuid(message = "Invalid account ID"))]
pub account_id: String,
#[validate(custom(function = "validate_transaction_type"))]
pub transaction_type: String,
#[validate(custom(function = "validate_money_amount"))]
pub gross_amount: String,
#[validate(custom(function = "validate_money_amount"))]
pub net_amount: String,
#[validate(length(equal = 3, message = "Currency must be 3-letter ISO code"))]
pub currency: String,
#[validate(length(min = 1, max = 255, message = "Description is required"))]
pub description: String,
#[validate(length(max = 100))]
pub merchant: Option<String>,
#[validate(custom(function = "validate_iso_date"))]
pub transaction_date: String,
#[validate(length(max = 10, message = "Maximum 10 tags allowed"))]
pub tag_ids: Vec<String>,
}
fn validate_transaction_type(tx_type: &str) -> Result<(), ValidationError> {
let valid_types = ["expense", "income", "transfer_out", "transfer_in"];
if valid_types.contains(&tx_type) {
Ok(())
} else {
Err(ValidationError::new("invalid_transaction_type"))
}
}
fn validate_money_amount(amount: &str) -> Result<(), ValidationError> {
if let Ok(dec) = Decimal::from_str(amount) {
if dec >= Decimal::ZERO {
// Check scale (decimal places)
if dec.scale() <= 8 {
Ok(())
} else {
let mut err = ValidationError::new("too_many_decimals");
err.message = Some("Amount must have at most 8 decimal places".into());
Err(err)
}
} else {
Err(ValidationError::new("negative_amount"))
}
} else {
Err(ValidationError::new("invalid_amount"))
}
}
fn validate_iso_date(date: &str) -> Result<(), ValidationError> {
if chrono::NaiveDate::parse_from_str(date, "%Y-%m-%d").is_ok() {
Ok(())
} else {
Err(ValidationError::new("invalid_date_format"))
}
}
```
### Goal DTO
```rust
// src-tauri/src/dto/goal_dto.rs
use validator::{Validate, ValidationError};
use serde::{Deserialize, Serialize};
use rust_decimal::Decimal;
#[derive(Debug, Serialize, Deserialize, Validate)]
pub struct CreateGoalDto {
#[validate(length(min = 1, max = 100))]
pub name: String,
#[validate(length(max = 500))]
pub description: Option<String>,
#[validate(custom(function = "validate_positive_decimal"))]
pub target_amount: String,
#[validate(length(equal = 3))]
pub currency: String,
#[validate(custom(function = "validate_goal_type"))]
pub goal_type: String,
#[validate(custom(function = "validate_optional_date"))]
pub target_date: Option<String>,
}
fn validate_goal_type(goal_type: &str) -> Result<(), ValidationError> {
let valid_types = ["savings", "debt_payoff", "spending_limit", "custom"];
if valid_types.contains(&goal_type) {
Ok(())
} else {
Err(ValidationError::new("invalid_goal_type"))
}
}
fn validate_positive_decimal(amount: &str) -> Result<(), ValidationError> {
if let Ok(dec) = Decimal::from_str(amount) {
if dec > Decimal::ZERO {
Ok(())
} else {
Err(ValidationError::new("must_be_positive"))
}
} else {
Err(ValidationError::new("invalid_decimal"))
}
}
fn validate_optional_date(date: &str) -> Result<(), ValidationError> {
validate_iso_date(date)
}
```
### Using Validation in Commands
```rust
// src-tauri/src/commands/accounts.rs
use crate::dto::account_dto::CreateAccountDto;
use validator::Validate;
#[tauri::command]
pub async fn create_account(
dto: CreateAccountDto,
db: State<'_, DatabaseConnection>,
) -> Result<Account, String> {
// Validate input
if let Err(errors) = dto.validate() {
let error_messages: Vec<String> = errors
.field_errors()
.iter()
.map(|(field, errs)| {
let messages: Vec<String> = errs
.iter()
.filter_map(|e| e.message.clone().map(|m| m.to_string()))
.collect();
format!("{}: {}", field, messages.join(", "))
})
.collect();
return Err(format!("Validation failed: {}", error_messages.join("; ")));
}
// Proceed with validated data
let account = account::ActiveModel {
id: Set(uuid::Uuid::new_v4().to_string()),
name: Set(dto.name),
account_type: Set(dto.account_type),
currency: Set(dto.currency),
initial_balance: Set(dto.initial_balance),
current_balance: Set(dto.initial_balance),
color: Set(dto.color),
created_at: Set(Utc::now()),
updated_at: Set(Utc::now()),
..Default::default()
};
account.insert(&**db).await.map_err(|e| e.to_string())
}
```
### Validation Error Handling
```rust
// src-tauri/src/error.rs
use validator::ValidationErrors;
#[derive(Error, Debug)]
pub enum AppError {
#[error("Validation error: {0}")]
Validation(ValidationErrors),
#[error("Database error: {0}")]
Database(#[from] sea_orm::DbErr),
// ... other errors
}
impl From<ValidationErrors> for AppError {
fn from(errors: ValidationErrors) -> Self {
AppError::Validation(errors)
}
}
// Convert validation errors to user-friendly messages
pub fn format_validation_errors(errors: &ValidationErrors) -> String {
errors
.field_errors()
.iter()
.flat_map(|(field, field_errors)| {
field_errors.iter().map(move |error| {
let message = error
.message
.clone()
.unwrap_or_else(|| format!("Invalid {}", field).into());
format!("{}: {}", field, message)
})
})
.collect::<Vec<_>>()
.join("; ")
}
```
### Complex Validation Example
```rust
// Validate that transfer amounts match with exchange rate
#[derive(Debug, Validate)]
pub struct CreateTransferDto {
#[validate(uuid)]
pub from_account_id: String,
#[validate(uuid)]
pub to_account_id: String,
#[validate(custom(function = "validate_money_amount"))]
pub from_amount: String,
#[validate(custom(function = "validate_money_amount"))]
pub to_amount: String,
pub exchange_rate: Option<String>,
#[validate(custom(function = "validate_different_accounts"))]
_phantom: (), // Forces validation function to run
}
fn validate_different_accounts(_: &()) -> Result<(), ValidationError> {
// This is a placeholder - actual check happens in command
Ok(())
}
// Better approach: Custom struct-level validation
impl CreateTransferDto {
pub fn validate_accounts(&self) -> Result<(), ValidationError> {
if self.from_account_id == self.to_account_id {
return Err(ValidationError::new("same_account"));
}
Ok(())
}
pub fn validate_exchange_rate(&self) -> Result<(), ValidationError> {
if let Some(rate_str) = &self.exchange_rate {
let rate = Decimal::from_str(rate_str)
.map_err(|_| ValidationError::new("invalid_rate"))?;
let from = Decimal::from_str(&self.from_amount)
.map_err(|_| ValidationError::new("invalid_from_amount"))?;
let to = Decimal::from_str(&self.to_amount)
.map_err(|_| ValidationError::new("invalid_to_amount"))?;
let expected_to = from * rate;
let tolerance = Decimal::from_str("0.01").unwrap();
if (expected_to - to).abs() > tolerance {
return Err(ValidationError::new("exchange_rate_mismatch"));
}
}
Ok(())
}
}
```
## Frontend Validation (TypeScript/Zod)
### Zod Schemas
```typescript
// src/lib/validation.ts
import { z } from 'zod';
export const moneySchema = z.string()
.regex(/^-?\d+\.?\d*$/, 'Invalid amount format')
.refine((val) => {
const num = parseFloat(val);
return !isNaN(num) && num >= 0;
}, 'Amount must be non-negative');
export const createAccountSchema = z.object({
name: z.string().min(1).max(100),
accountType: z.enum(['checking', 'savings', 'credit_card', 'cash', 'digital_wallet', 'loan', 'other']),
currency: z.string().length(3),
initialBalance: moneySchema,
color: z.string().regex(/^#[0-9A-Fa-f]{6}$/).optional(),
});
export const createTransactionSchema = z.object({
accountId: z.string().uuid(),
transactionType: z.enum(['expense', 'income', 'transfer_out', 'transfer_in']),
grossAmount: moneySchema,
taxAmount: moneySchema.optional(),
netAmount: moneySchema,
currency: z.string().length(3),
description: z.string().min(1).max(255),
merchant: z.string().max(100).optional(),
transactionDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
tagIds: z.array(z.string().uuid()).max(10),
});
export type CreateAccountInput = z.infer<typeof createAccountSchema>;
export type CreateTransactionInput = z.infer<typeof createTransactionSchema>;
```
### React Hook Form Integration
```typescript
// src/components/forms/account-form.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { createAccountSchema, CreateAccountInput } from '@/lib/validation';
export function AccountForm() {
const form = useForm<CreateAccountInput>({
resolver: zodResolver(createAccountSchema),
defaultValues: {
name: '',
accountType: 'checking',
currency: 'HKD',
initialBalance: '0.00',
},
});
const onSubmit = async (data: CreateAccountInput) => {
try {
await invoke('create_account', data);
toast.success('Account created');
} catch (error) {
toast.error(error as string);
}
};
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
{/* Form fields */}
</form>
);
}
```
## Validation Rules Summary
### Account
- Name: 1-100 characters, required
- Type: Must be valid enum value
- Currency: 3-letter ISO code
- Initial Balance: Valid decimal, max 8 decimal places, non-negative
- Color: Valid hex color (#RRGGBB)
### Transaction
- Account ID: Valid UUID
- Type: Must be valid enum value
- Amounts: Valid decimal, max 8 decimal places, non-negative
- Currency: 3-letter ISO code
- Description: 1-255 characters, required
- Merchant: Max 100 characters, optional
- Date: YYYY-MM-DD format
- Tags: Max 10 tags
### Transfer
- From/To Account: Valid UUIDs, must be different
- Amounts: Valid decimals, max 8 decimal places
- Exchange Rate: Valid decimal (optional)
- Amounts must match within tolerance if rate provided
### Goal
- Name: 1-100 characters, required
- Description: Max 500 characters
- Target Amount: Valid positive decimal
- Currency: 3-letter ISO code
- Type: Must be valid enum value
- Target Date: YYYY-MM-DD format (optional)
## Best Practices
1. **Validate on both sides**: Frontend for UX, backend for security
2. **Use DTOs**: Separate validation structs from database models
3. **Custom validators**: For business logic (e.g., account types, date formats)
4. **Error messages**: Clear, user-friendly messages
5. **Decimal precision**: Always validate 8 decimal places max
6. **UUID validation**: Ensure foreign keys are valid UUIDs
7. **Enum validation**: Always check against allowed values
8. **Sanitize input**: Trim strings, normalize case where appropriate