30 KiB
30 KiB
Technical Specifications
Technology Stack
Frontend
Core Framework:
- React 18.2+ - UI library
- TypeScript 5.0+ - Type safety
- Vite 5.0+ - Build tool and dev server
- SWC - Fast compilation
UI Framework:
- shadcn/ui - Headless UI components
- Tailwind CSS 3.4+ - Utility-first CSS
- Radix UI - Primitives (accessibility)
- Lucide React - Icons
- Framer Motion - Animations
State Management:
- TanStack Query (React Query) v5 - Server state
- Zustand 4.4+ - Client state
Forms & Validation:
- React Hook Form 7.49+ - Form management
- Zod 3.22+ - Schema validation
- @hookform/resolvers - Zod integration
Internationalization:
- react-i18next 13.5+ - i18n framework
- i18next 23.7+ - Core library
Date & Time:
- date-fns 3.0+ - Date manipulation
- date-fns-tz - Timezone support
Utilities:
- clsx / tailwind-merge - Class name utilities
- uuid - UUID generation
- lodash-es - Utility functions (tree-shakeable)
Backend (Rust)
Core Framework:
- Tauri 2.0 - Desktop/mobile framework
- tokio 1.35+ - Async runtime
- serde 1.0+ - Serialization
Database:
- SeaORM 1.0+ - Async ORM for SQLite
- sea-orm-migration - Database migrations
- sqlx-sqlite - Underlying SQLite driver
- sea-orm-cli - CLI tools for entity generation
Financial:
- rust_decimal 1.33+ - 128-bit decimal arithmetic
- rust_decimal_macros 1.33+ - Decimal literals
Time:
- chrono 0.4+ - Date/time handling
- chrono-tz 0.8+ - Timezone database
Error Handling:
- thiserror 1.0+ - Error types
- anyhow 1.0+ - Error propagation
Async:
- tokio 1.35+ - Runtime and utilities
- futures 0.3+ - Async utilities
HTTP (for OCR API):
- reqwest 0.11+ - HTTP client
Other:
- uuid 1.6+ - UUID generation
- serde_json 1.0+ - JSON handling
- dirs 5.0+ - Platform directories
Project Structure
finance-tracker/
├── src/ # Frontend source
│ ├── components/
│ │ ├── ui/ # shadcn/ui components
│ │ │ ├── button.tsx
│ │ │ ├── card.tsx
│ │ │ ├── input.tsx
│ │ │ ├── select.tsx
│ │ │ └── ...
│ │ ├── forms/
│ │ │ ├── transaction-form.tsx
│ │ │ ├── account-form.tsx
│ │ │ ├── transfer-form.tsx
│ │ │ └── goal-form.tsx
│ │ ├── layout/
│ │ │ ├── bottom-nav.tsx
│ │ │ ├── header.tsx
│ │ │ └── page-wrapper.tsx
│ │ ├── transactions/
│ │ │ ├── transaction-list.tsx
│ │ │ ├── transaction-card.tsx
│ │ │ └── transaction-filters.tsx
│ │ ├── accounts/
│ │ │ ├── account-list.tsx
│ │ │ ├── account-card.tsx
│ │ │ └── account-summary.tsx
│ │ ├── goals/
│ │ │ ├── goal-list.tsx
│ │ │ ├── goal-card.tsx
│ │ │ └── goal-progress.tsx
│ │ └── common/
│ │ ├── amount-display.tsx
│ │ ├── tag-input.tsx
│ │ ├── date-picker.tsx
│ │ └── color-picker.tsx
│ ├── pages/
│ │ ├── dashboard.tsx
│ │ ├── accounts.tsx
│ │ ├── account-detail.tsx
│ │ ├── transactions.tsx
│ │ ├── transaction-detail.tsx
│ │ ├── add-transaction.tsx
│ │ ├── transfers.tsx
│ │ ├── goals.tsx
│ │ ├── goal-detail.tsx
│ │ ├── scheduled.tsx
│ │ ├── scheduled-detail.tsx
│ │ ├── settings.tsx
│ │ └── settings-*.tsx
│ ├── hooks/
│ │ ├── use-accounts.ts
│ │ ├── use-transactions.ts
│ │ ├── use-goals.ts
│ │ ├── use-tags.ts
│ │ ├── use-settings.ts
│ │ ├── use-currency.ts
│ │ └── use-theme.ts
│ ├── stores/
│ │ ├── settings-store.ts
│ │ └── ui-store.ts
│ ├── lib/
│ │ ├── utils.ts
│ │ ├── currency.ts
│ │ ├── validation.ts
│ │ ├── constants.ts
│ │ └── api.ts
│ ├── types/
│ │ ├── models.ts
│ │ ├── api.ts
│ │ └── enums.ts
│ ├── i18n/
│ │ ├── index.ts
│ │ ├── en.json
│ │ └── zh-Hant.json
│ ├── App.tsx
│ ├── main.tsx
│ ├── index.css
│ └── vite-env.d.ts
├── src-tauri/
│ ├── src/
│ │ ├── main.rs
│ │ ├── lib.rs
│ │ ├── commands/
│ │ │ ├── mod.rs
│ │ │ ├── accounts.rs
│ │ │ ├── transactions.rs
│ │ │ ├── tags.rs
│ │ │ ├── goals.rs
│ │ │ ├── scheduled.rs
│ │ │ ├── transfers.rs
│ │ │ ├── reconciliation.rs
│ │ │ └── backup.rs
│ │ ├── db/
│ │ │ ├── mod.rs
│ │ │ └── connection.rs
│ │ ├── entities/
│ │ │ ├── mod.rs
│ │ │ ├── prelude.rs
│ │ │ ├── account.rs
│ │ │ ├── tag.rs
│ │ │ ├── transaction.rs
│ │ │ ├── transaction_tag.rs
│ │ │ ├── scheduled_transaction.rs
│ │ │ ├── scheduled_instance.rs
│ │ │ ├── goal.rs
│ │ │ ├── goal_rule.rs
│ │ │ ├── goal_progress.rs
│ │ │ ├── transfer.rs
│ │ │ ├── exchange_rate.rs
│ │ │ ├── reconciliation.rs
│ │ │ └── setting.rs
│ │ ├── migrations/
│ │ │ ├── mod.rs
│ │ │ ├── m001_initial.rs
│ │ │ └── ...
│ │ ├── models/
│ │ │ ├── mod.rs
│ │ │ ├── money.rs
│ │ │ ├── account.rs
│ │ │ ├── transaction.rs
│ │ │ ├── tag.rs
│ │ │ ├── goal.rs
│ │ │ ├── scheduled.rs
│ │ │ └── settings.rs
│ │ ├── services/
│ │ │ ├── mod.rs
│ │ │ ├── scheduler.rs
│ │ │ ├── goal_engine.rs
│ │ │ ├── transfer_service.rs
│ │ │ ├── exchange_rate.rs
│ │ │ └── backup.rs
│ │ └── error.rs
│ ├── capabilities/
│ │ ├── default.json
│ │ └── mobile.json
│ ├── gen/ # Generated mobile files
│ ├── Cargo.toml
│ └── build.rs
├── doc/ # Documentation
├── public/
│ ├── icons/
│ └── images/
├── package.json
├── tsconfig.json
├── tsconfig.node.json
├── tailwind.config.js
├── tailwind.config.ts
├── vite.config.ts
├── components.json # shadcn/ui config
├── index.html
└── README.md
Data Models
TypeScript Models
// types/models.ts
export type UUID = string;
export enum Currency {
HKD = 'HKD',
USD = 'USD',
CNY = 'CNY',
EUR = 'EUR',
GBP = 'GBP',
JPY = 'JPY',
BTC = 'BTC',
ETH = 'ETH',
}
export enum AccountType {
CHECKING = 'checking',
SAVINGS = 'savings',
CREDIT_CARD = 'credit_card',
CASH = 'cash',
DIGITAL_WALLET = 'digital_wallet',
LOAN = 'loan',
OTHER = 'other',
}
export enum TransactionType {
EXPENSE = 'expense',
INCOME = 'income',
TRANSFER_OUT = 'transfer_out',
TRANSFER_IN = 'transfer_in',
}
export interface Money {
amount: string; // 8 decimal precision
currency: Currency;
}
export interface Account {
id: UUID;
name: string;
accountType: AccountType;
currency: Currency;
initialBalance: string;
currentBalance: string;
color: string;
icon: string;
sortOrder: number;
isActive: boolean;
isArchived: boolean;
includeInNetWorth: boolean;
showInCombinedView: boolean;
createdAt: string;
updatedAt: string;
version: number;
deviceId?: string;
isDeleted: boolean;
}
export interface Tag {
id: UUID;
name: string;
color: string;
icon?: string;
budgetAmount?: string;
budgetPeriod?: 'daily' | 'weekly' | 'monthly' | 'yearly';
isSystem: boolean;
sortOrder: number;
createdAt: string;
updatedAt: string;
version: number;
deviceId?: string;
isDeleted: boolean;
}
export interface Transaction {
id: UUID;
accountId: UUID;
transactionType: TransactionType;
grossAmount: string;
taxAmount: string;
netAmount: string;
taxRate?: string;
currency: Currency;
description: string;
merchant?: string;
notes?: string;
receiptPaths?: string[];
receiptOcrData?: Record<string, unknown>;
transferId?: UUID;
relatedTransactionId?: UUID;
scheduleId?: UUID;
isScheduledInstance: boolean;
isAutoInserted: boolean;
needsReview: boolean;
transactionDate: string; // YYYY-MM-DD
createdAt: string;
updatedAt: string;
version: number;
deviceId?: string;
isDeleted: boolean;
syncStatus: 'synced' | 'pending' | 'conflict' | 'error';
tags?: Tag[];
}
export interface ScheduledTransaction {
id: UUID;
accountId: UUID;
scheduleType: 'daily' | 'weekly' | 'monthly' | 'yearly' | 'custom';
frequency: number;
daysOfWeek?: number[];
dayOfMonth?: number;
monthOfYear?: number;
executionTime: string; // HH:MM
timezone?: string;
startDate: string;
endDate?: string;
occurrenceCount?: number;
currentOccurrence: number;
transactionType: TransactionType;
grossAmount: string;
taxAmount: string;
netAmount: string;
currency: Currency;
description?: string;
merchant?: string;
notes?: string;
tagIds?: UUID[];
isActive: boolean;
lastGeneratedDate?: string;
nextExecutionDatetime: string;
createdAt: string;
updatedAt: string;
version: number;
deviceId?: string;
isDeleted: boolean;
}
export interface Goal {
id: UUID;
name: string;
description?: string;
targetAmount: string;
currentAmount: string;
currency: Currency;
goalType: 'savings' | 'debt_payoff' | 'spending_limit' | 'custom';
targetDate?: string;
isRecurring: boolean;
recurrencePeriod?: 'monthly' | 'quarterly' | 'yearly';
linkedAccountId?: UUID;
color?: string;
icon?: string;
isActive: boolean;
isAchieved: boolean;
achievedAt?: string;
lastResetDate?: string;
createdAt: string;
updatedAt: string;
version: number;
deviceId?: string;
isDeleted: boolean;
rules?: GoalRule[];
progress?: GoalProgress[];
}
export interface GoalRule {
id: UUID;
goalId: UUID;
tagIds: UUID[];
contributionType: 'percentage' | 'fixed';
percentage?: string;
fixedAmount?: string;
maxContributionPerTransaction?: string;
monthlyCap?: string;
isActive: boolean;
createdAt: string;
updatedAt: string;
version: number;
deviceId?: string;
isDeleted: boolean;
}
export interface Settings {
language: 'en' | 'zh-Hant';
defaultCurrency: Currency;
baseCurrency: Currency;
timezone: string;
defaultView: 'combined' | 'single';
displayMode: 'net' | 'gross';
decimalPlaces: number;
dateFormat: string;
timeFormat: '12h' | '24h';
theme: 'light' | 'dark' | 'system';
weekStartsOn: 0 | 1; // 0=Sun, 1=Mon
scheduledCheckInterval: number;
}
Tauri Commands
Account Commands
#[tauri::command]
async fn create_account(
name: String,
account_type: AccountType,
currency: Currency,
initial_balance: String,
color: Option<String>,
icon: Option<String>,
) -> Result<Account, Error>
#[tauri::command]
async fn get_accounts(
include_archived: bool,
) -> Result<Vec<Account>, Error>
#[tauri::command]
async fn get_account(
id: String,
) -> Result<Account, Error>
#[tauri::command]
async fn update_account(
id: String,
updates: AccountUpdate,
) -> Result<Account, Error>
#[tauri::command]
async fn archive_account(
id: String,
archived: bool,
) -> Result<(), Error>
#[tauri::command]
async fn delete_account(
id: String,
) -> Result<(), Error>
#[tauri::command]
async fn get_account_balance(
id: String,
as_of_date: Option<String>,
) -> Result<Money, Error>
Transaction Commands
#[tauri::command]
async fn create_transaction(
account_id: String,
transaction_type: TransactionType,
gross_amount: String,
tax_amount: Option<String>,
net_amount: String,
currency: Currency,
description: String,
merchant: Option<String>,
notes: Option<String>,
tag_ids: Vec<String>,
transaction_date: String,
receipt_paths: Option<Vec<String>>,
) -> Result<Transaction, Error>
#[tauri::command]
async fn get_transactions(
filter: TransactionFilter,
) -> Result<Vec<Transaction>, Error>
#[tauri::command]
async fn get_transaction(
id: String,
) -> Result<Transaction, Error>
#[tauri::command]
async fn update_transaction(
id: String,
updates: TransactionUpdate,
) -> Result<Transaction, Error>
#[tauri::command]
async fn delete_transaction(
id: String,
) -> Result<(), Error>
#[tauri::command]
async fn get_transactions_needing_review(
) -> Result<Vec<Transaction>, Error>
#[tauri::command]
async fn confirm_transaction(
id: String,
) -> Result<(), Error>
Tag Commands
#[tauri::command]
async fn create_tag(
name: String,
color: String,
icon: Option<String>,
budget_amount: Option<String>,
budget_period: Option<String>,
) -> Result<Tag, Error>
#[tauri::command]
async fn get_tags(
include_system: bool,
) -> Result<Vec<Tag>, Error>
#[tauri::command]
async fn update_tag(
id: String,
updates: TagUpdate,
) -> Result<Tag, Error>
#[tauri::command]
async fn delete_tag(
id: String,
) -> Result<(), Error>
Scheduled Transaction Commands
#[tauri::command]
async fn create_scheduled_transaction(
account_id: String,
schedule_type: String,
frequency: i32,
days_of_week: Option<Vec<i32>>,
day_of_month: Option<i32>,
execution_time: String,
start_date: String,
end_date: Option<String>,
occurrence_count: Option<i32>,
transaction_type: TransactionType,
gross_amount: String,
tax_amount: Option<String>,
net_amount: String,
currency: Currency,
description: Option<String>,
merchant: Option<String>,
notes: Option<String>,
tag_ids: Vec<String>,
) -> Result<ScheduledTransaction, Error>
#[tauri::command]
async fn get_scheduled_transactions(
include_inactive: bool,
) -> Result<Vec<ScheduledTransaction>, Error>
#[tauri::command]
async fn update_scheduled_transaction(
id: String,
updates: ScheduledTransactionUpdate,
) -> Result<ScheduledTransaction, Error>
#[tauri::command]
async fn delete_scheduled_transaction(
id: String,
) -> Result<(), Error>
#[tauri::command]
async fn skip_scheduled_instance(
schedule_id: String,
due_date: String,
) -> Result<(), Error>
#[tauri::command]
async fn process_due_schedules(
) -> Result<Vec<Transaction>, Error>
Goal Commands
#[tauri::command]
async fn create_goal(
name: String,
description: Option<String>,
target_amount: String,
currency: Currency,
goal_type: String,
target_date: Option<String>,
is_recurring: bool,
linked_account_id: Option<String>,
color: Option<String>,
icon: Option<String>,
) -> Result<Goal, Error>
#[tauri::command]
async fn get_goals(
include_achieved: bool,
) -> Result<Vec<Goal>, Error>
#[tauri::command]
async fn update_goal(
id: String,
updates: GoalUpdate,
) -> Result<Goal, Error>
#[tauri::command]
async fn delete_goal(
id: String,
) -> Result<(), Error>
#[tauri::command]
async fn create_goal_rule(
goal_id: String,
tag_ids: Vec<String>,
contribution_type: String,
percentage: Option<String>,
fixed_amount: Option<String>,
max_contribution: Option<String>,
monthly_cap: Option<String>,
) -> Result<GoalRule, Error>
#[tauri::command]
async fn manual_contribute_to_goal(
goal_id: String,
amount: String,
notes: Option<String>,
) -> Result<(), Error>
Transfer Commands
#[tauri::command]
async fn create_transfer(
from_account_id: String,
to_account_id: String,
from_amount: String,
to_amount: String,
exchange_rate: Option<String>,
exchange_rate_source: Option<String>,
fees: Option<String>,
description: Option<String>,
transfer_date: String,
) -> Result<Transfer, Error>
#[tauri::command]
async fn get_transfers(
account_id: Option<String>,
start_date: Option<String>,
end_date: Option<String>,
) -> Result<Vec<Transfer>, Error>
Settings Commands
#[tauri::command]
async fn get_settings(
) -> Result<Settings, Error>
#[tauri::command]
async fn update_setting(
key: String,
value: String,
) -> Result<(), Error>
#[tauri::command]
async fn update_settings(
settings: SettingsUpdate,
) -> Result<(), Error>
Backup Commands
#[tauri::command]
async fn export_to_json(
path: String,
) -> Result<(), Error>
#[tauri::command]
async fn import_from_json(
path: String,
) -> Result<ImportReport, Error>
#[tauri::command]
async fn get_database_path(
) -> Result<String, Error>
State Management
React Query (Server State)
// hooks/use-accounts.ts
export function useAccounts() {
return useQuery({
queryKey: ['accounts'],
queryFn: () => invoke<Account[]>('get_accounts', { includeArchived: false }),
});
}
// hooks/use-transactions.ts
export function useTransactions(filter: TransactionFilter) {
return useQuery({
queryKey: ['transactions', filter],
queryFn: () => invoke<Transaction[]>('get_transactions', { filter }),
});
}
// hooks/use-create-transaction.ts
export function useCreateTransaction() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateTransactionInput) =>
invoke<Transaction>('create_transaction', data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['transactions'] });
queryClient.invalidateQueries({ queryKey: ['accounts'] });
queryClient.invalidateQueries({ queryKey: ['goals'] });
},
});
}
Zustand (Client State)
// stores/settings-store.ts
interface SettingsState {
settings: Settings | null;
isLoading: boolean;
fetchSettings: () => Promise<void>;
updateSetting: (key: string, value: string) => Promise<void>;
}
export const useSettingsStore = create<SettingsState>((set, get) => ({
settings: null,
isLoading: false,
fetchSettings: async () => {
set({ isLoading: true });
const settings = await invoke<Settings>('get_settings');
set({ settings, isLoading: false });
},
updateSetting: async (key, value) => {
await invoke('update_setting', { key, value });
await get().fetchSettings();
},
}));
// stores/ui-store.ts
interface UIState {
theme: 'light' | 'dark' | 'system';
sidebarOpen: boolean;
activeAccountId: string | null;
setTheme: (theme: UIState['theme']) => void;
toggleSidebar: () => void;
setActiveAccount: (id: string | null) => void;
}
export const useUIStore = create<UIState>((set) => ({
theme: 'system',
sidebarOpen: false,
activeAccountId: null,
setTheme: (theme) => set({ theme }),
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
setActiveAccount: (id) => set({ activeAccountId: id }),
}));
Error Handling
Rust Error Types
// src/error.rs
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("Database error: {0}")]
Database(#[from] rusqlite::Error),
#[error("Validation error: {0}")]
Validation(String),
#[error("Not found: {0}")]
NotFound(String),
#[error("Invalid amount: {0}")]
InvalidAmount(String),
#[error("Currency mismatch: expected {expected}, got {actual}")]
CurrencyMismatch { expected: String, actual: String },
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("Unknown error: {0}")]
Unknown(String),
}
pub type Result<T> = std::result::Result<T, AppError>;
// Convert to string for Tauri
impl From<AppError> for String {
fn from(err: AppError) -> Self {
err.to_string()
}
}
Frontend Error Handling
// lib/error-handling.ts
import { toast } from 'sonner';
export function handleError(error: unknown, context?: string) {
const message = error instanceof Error ? error.message : String(error);
console.error(`[${context}]`, error);
toast.error(context ? `${context}: ${message}` : message);
}
// In components
const { mutate, isPending } = useCreateTransaction({
onError: (error) => handleError(error, 'Failed to create transaction'),
});
Validation
Zod Schemas
// 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 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.nativeEnum(Currency),
description: z.string().min(1, 'Description is required'),
merchant: z.string().optional(),
notes: z.string().optional(),
tagIds: z.array(z.string().uuid()),
transactionDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), // YYYY-MM-DD
});
export type CreateTransactionInput = z.infer<typeof createTransactionSchema>;
// Usage in forms
const form = useForm<CreateTransactionInput>({
resolver: zodResolver(createTransactionSchema),
});
Performance Optimizations
Frontend
-
Virtual Lists
- Use
@tanstack/react-virtualfor long transaction lists - Render only visible items
- Use
-
Image Optimization
- Compress receipt images before storage
- Thumbnails for gallery view
- Lazy load full-size images
-
Code Splitting
- Route-based splitting
- Lazy load heavy components (charts)
-
Query Optimization
- Stale-while-revalidate
- Optimistic updates
- Proper invalidation
-
Debouncing
- Search inputs: 300ms
- Amount inputs: 100ms
Backend
-
Database
- Proper indexes (see database.md)
- WAL mode enabled
- Prepared statements
- Connection pooling (if needed)
-
Query Optimization
- Limit/offset for pagination
- Select specific columns
- Avoid N+1 queries
-
Caching
- Cache account balances
- Cache exchange rates
- In-memory LRU cache
-
Background Tasks
- Scheduled transaction checker
- Exchange rate fetcher
- Run in separate thread
Security
Data Protection
-
Database Encryption
- Optional SQLCipher
- Encryption key in OS keychain
- Prompt for password on first launch
-
Local Storage
- App data directory only
- No cloud sync in MVP
- User controls backup location
-
Receipts
- Local filesystem only
- No cloud upload
- Optional file encryption
API Keys
- OCR Service
- Google Vision API key
- Store in app config (not in code)
- Allow user to provide own key
Testing Strategy
Frontend Tests
// Unit tests with Vitest
// Component tests with React Testing Library
// E2E tests with Playwright
// Example: TransactionForm.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { TransactionForm } from './transaction-form';
describe('TransactionForm', () => {
it('submits valid transaction', async () => {
const onSubmit = vi.fn();
render(<TransactionForm onSubmit={onSubmit} />);
fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: '100.00' },
});
fireEvent.change(screen.getByLabelText('Description'), {
target: { value: 'Test transaction' },
});
fireEvent.click(screen.getByText('Save'));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
amount: '100.00',
description: 'Test transaction',
// ...
});
});
});
});
Backend Tests
// Unit tests for business logic
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_money_calculation() {
let amount = Money::new(dec!(100.50), Currency::HKD);
let tax = Money::new(dec!(8.25), Currency::HKD);
let total = amount + tax;
assert_eq!(total.amount, dec!(108.75));
}
}
// Integration tests for database
#[tokio::test]
async fn test_create_transaction() {
let db = setup_test_db().await;
let transaction = create_transaction(&db, /* ... */).await.unwrap();
assert_eq!(transaction.description, "Test");
assert_eq!(transaction.net_amount, "100.00000000");
}
Build Configuration
Vite Config
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import path from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
build: {
target: 'esnext',
minify: 'terser',
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
ui: ['@radix-ui/react-dialog', '@radix-ui/react-select'],
},
},
},
},
});
Tauri Config
{
"build": {
"beforeBuildCommand": "npm run build",
"beforeDevCommand": "npm run dev",
"devPath": "http://localhost:5173",
"distDir": "../dist"
},
"tauri": {
"allowlist": {
"all": false,
"fs": {
"all": false,
"readFile": true,
"writeFile": true,
"readDir": true,
"copyFile": true,
"createDir": true,
"removeDir": true,
"removeFile": true,
"renameFile": true
},
"path": {
"all": true
},
"dialog": {
"all": false,
"open": true,
"save": true
},
"shell": {
"all": false,
"open": true
}
},
"bundle": {
"active": true,
"category": "Finance",
"copyright": "",
"deb": {
"depends": []
},
"externalBin": [],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"identifier": "com.finance-tracker.app",
"longDescription": "",
"macOS": {
"entitlements": null,
"exceptionDomain": "",
"frameworks": [],
"providerShortName": null,
"signingIdentity": null
},
"resources": [],
"shortDescription": "",
"targets": "all",
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
}
},
"security": {
"csp": null
},
"updater": {
"active": false
},
"windows": [
{
"fullscreen": false,
"height": 800,
"resizable": true,
"title": "Finance Tracker",
"width": 1200
}
]
}
}
Development Workflow
Getting Started
# 1. Clone repository
git clone <repo>
cd finance-tracker
# 2. Install dependencies
npm install
cd src-tauri && cargo build
# 3. Run dev server
npm run tauri dev
# 4. For iOS
cd src-tauri
npm run tauri ios dev
# 5. For Android
cd src-tauri
npm run tauri android dev
Code Quality
# Linting
npm run lint
# Type checking
npm run typecheck
# Formatting
npm run format
# Testing
npm run test
npm run test:e2e
SeaORM CLI
# Install sea-orm-cli
cargo install sea-orm-cli
# Generate entities from database (reverse engineering)
sea-orm-cli generate entity \
-u sqlite:///path/to/db.sqlite \
-o src-tauri/src/entities
# Run migrations
sea-orm-cli migrate up
# Create new migration
sea-orm-cli migrate generate m002_add_new_table
# Check migration status
sea-orm-cli migrate status
Git Workflow
main (production)
↓
develop (integration)
↓
feature/* (feature branches)
Branch Naming:
feature/transaction-filtersfix/decimal-precisiondocs/api-reference
Commit Convention:
feat: add transaction filteringfix: correct decimal calculationdocs: update API reference
Cargo.toml Dependencies
[package]
name = "finance-tracker"
version = "1.0.0"
edition = "2021"
rust-version = "1.75"
[dependencies]
# Tauri
tauri = { version = "2.0", features = [] }
tauri-plugin-shell = "2.0"
tauri-plugin-dialog = "2.0"
tauri-plugin-fs = "2.0"
# SeaORM
sea-orm = { version = "1.0", features = ["sqlx-sqlite", "runtime-tokio-native-tls", "macros"] }
sea-orm-migration = "1.0"
# Async Runtime
tokio = { version = "1.35", features = ["full"] }
# Serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
# Financial Precision
rust_decimal = "1.33"
rust_decimal_macros = "1.33"
# Time
chrono = { version = "0.4", features = ["serde"] }
# Utilities
uuid = { version = "1.6", features = ["v4", "serde"] }
thiserror = "1.0"
anyhow = "1.0"
dirs = "5.0"
# Validation
validator = { version = "0.16", features = ["derive"] }
# HTTP Client (for OCR API)
reqwest = { version = "0.11", features = ["json"] }
[dev-dependencies]
tokio-test = "0.4"
sea-orm = { version = "1.0", features = ["mock"] }
Deployment
iOS
- Build:
npm run tauri ios build - Archive in Xcode
- Upload to App Store Connect
- Submit for review
Android
- Build:
npm run tauri android build - Sign APK/AAB
- Upload to Google Play Console
- Submit for review
Web
- Build:
npm run tauri build --target web - Deploy static files to hosting
- Configure PWA manifest (optional)
Monitoring & Analytics (Future)
- Crash Reporting: Sentry integration
- Performance: React DevTools Profiler
- Analytics: Optional opt-in usage tracking
- Error Tracking: Automatic error reporting
Documentation Maintenance
Update these docs when:
- New features added
- API changes
- Database schema updates
- Architecture decisions changed
Version: 1.0.0
Last Updated: 2026-02-13
Status: Ready for implementation