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:
119
doc/README.md
Normal file
119
doc/README.md
Normal 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
922
doc/api-reference.md
Normal 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
523
doc/architecture.md
Normal 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
921
doc/database.md
Normal 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
935
doc/features.md
Normal 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
1357
doc/technical-specs.md
Normal file
File diff suppressed because it is too large
Load Diff
852
doc/ui-design.md
Normal file
852
doc/ui-design.md
Normal 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
469
doc/validation.md
Normal 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
|
||||
Reference in New Issue
Block a user