Files
finwise/doc/technical-specs.md
2026-02-13 12:05:11 +00:00

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

  1. Virtual Lists

    • Use @tanstack/react-virtual for long transaction lists
    • Render only visible items
  2. Image Optimization

    • Compress receipt images before storage
    • Thumbnails for gallery view
    • Lazy load full-size images
  3. Code Splitting

    • Route-based splitting
    • Lazy load heavy components (charts)
  4. Query Optimization

    • Stale-while-revalidate
    • Optimistic updates
    • Proper invalidation
  5. Debouncing

    • Search inputs: 300ms
    • Amount inputs: 100ms

Backend

  1. Database

    • Proper indexes (see database.md)
    • WAL mode enabled
    • Prepared statements
    • Connection pooling (if needed)
  2. Query Optimization

    • Limit/offset for pagination
    • Select specific columns
    • Avoid N+1 queries
  3. Caching

    • Cache account balances
    • Cache exchange rates
    • In-memory LRU cache
  4. Background Tasks

    • Scheduled transaction checker
    • Exchange rate fetcher
    • Run in separate thread

Security

Data Protection

  1. Database Encryption

    • Optional SQLCipher
    • Encryption key in OS keychain
    • Prompt for password on first launch
  2. Local Storage

    • App data directory only
    • No cloud sync in MVP
    • User controls backup location
  3. Receipts

    • Local filesystem only
    • No cloud upload
    • Optional file encryption

API Keys

  1. 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-filters
  • fix/decimal-precision
  • docs/api-reference

Commit Convention:

  • feat: add transaction filtering
  • fix: correct decimal calculation
  • docs: 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

  1. Build: npm run tauri ios build
  2. Archive in Xcode
  3. Upload to App Store Connect
  4. Submit for review

Android

  1. Build: npm run tauri android build
  2. Sign APK/AAB
  3. Upload to Google Play Console
  4. Submit for review

Web

  1. Build: npm run tauri build --target web
  2. Deploy static files to hosting
  3. 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