-- Monemalist Database Schema for Cloudflare D1 -- This schema extends Auth.js tables with application-specific tables -- Categories for expense classification CREATE TABLE IF NOT EXISTS categories ( id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), name TEXT NOT NULL, icon TEXT, color TEXT, type TEXT DEFAULT 'expense' CHECK (type IN ('expense', 'income')), user_id TEXT, is_system BOOLEAN DEFAULT FALSE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); -- Wallets (personal or group) CREATE TABLE IF NOT EXISTS wallets ( id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), name TEXT NOT NULL, type TEXT DEFAULT 'personal' CHECK (type IN ('personal', 'group', 'trip', 'business')), currency TEXT DEFAULT 'MYR', balance REAL DEFAULT 0, description TEXT, owner_id TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE ); -- Wallet members for group wallets CREATE TABLE IF NOT EXISTS wallet_members ( id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), wallet_id TEXT NOT NULL, user_id TEXT NOT NULL, role TEXT DEFAULT 'member' CHECK (role IN ('owner', 'admin', 'member', 'viewer')), joined_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (wallet_id) REFERENCES wallets(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, UNIQUE(wallet_id, user_id) ); -- Core transactions table CREATE TABLE IF NOT EXISTS transactions ( id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), wallet_id TEXT NOT NULL, user_id TEXT NOT NULL, type TEXT NOT NULL CHECK (type IN ('expense', 'income', 'transfer')), amount REAL NOT NULL, currency TEXT DEFAULT 'MYR', category_id TEXT, description TEXT, notes TEXT, date DATETIME DEFAULT CURRENT_TIMESTAMP, is_recurring BOOLEAN DEFAULT FALSE, recurring_id TEXT, location TEXT, merchant TEXT, tags TEXT, attachments TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (wallet_id) REFERENCES wallets(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL ); -- Recurring transaction templates CREATE TABLE IF NOT EXISTS recurring_transactions ( id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), wallet_id TEXT NOT NULL, user_id TEXT NOT NULL, type TEXT NOT NULL CHECK (type IN ('expense', 'income')), amount REAL NOT NULL, currency TEXT DEFAULT 'MYR', category_id TEXT, description TEXT NOT NULL, frequency TEXT NOT NULL CHECK (frequency IN ('daily', 'weekly', 'monthly', 'yearly')), day_of_month INTEGER, day_of_week INTEGER, start_date DATE NOT NULL, end_date DATE, last_processed DATE, is_active BOOLEAN DEFAULT TRUE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (wallet_id) REFERENCES wallets(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL ); -- Split transactions for group expenses CREATE TABLE IF NOT EXISTS transaction_splits ( id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), transaction_id TEXT NOT NULL, user_id TEXT NOT NULL, amount REAL NOT NULL, is_paid BOOLEAN DEFAULT FALSE, paid_at DATETIME, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (transaction_id) REFERENCES transactions(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, UNIQUE(transaction_id, user_id) ); -- Clients/Suppliers for business features CREATE TABLE IF NOT EXISTS contacts ( id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), user_id TEXT NOT NULL, type TEXT NOT NULL CHECK (type IN ('client', 'supplier', 'both')), name TEXT NOT NULL, email TEXT, phone TEXT, address TEXT, tax_number TEXT, notes TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); -- Invoices CREATE TABLE IF NOT EXISTS invoices ( id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), invoice_number TEXT NOT NULL, user_id TEXT NOT NULL, contact_id TEXT, wallet_id TEXT, status TEXT DEFAULT 'draft' CHECK (status IN ('draft', 'sent', 'paid', 'partial', 'overdue', 'cancelled')), issue_date DATE NOT NULL, due_date DATE NOT NULL, currency TEXT DEFAULT 'MYR', subtotal REAL NOT NULL, tax_rate REAL DEFAULT 0, tax_amount REAL DEFAULT 0, total REAL NOT NULL, paid_amount REAL DEFAULT 0, notes TEXT, terms TEXT, sent_at DATETIME, paid_at DATETIME, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE SET NULL, FOREIGN KEY (wallet_id) REFERENCES wallets(id) ON DELETE SET NULL, UNIQUE(invoice_number, user_id) ); -- Invoice line items CREATE TABLE IF NOT EXISTS invoice_items ( id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), invoice_id TEXT NOT NULL, description TEXT NOT NULL, quantity REAL DEFAULT 1, unit_price REAL NOT NULL, amount REAL NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE CASCADE ); -- Budgets CREATE TABLE IF NOT EXISTS budgets ( id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), wallet_id TEXT NOT NULL, category_id TEXT, name TEXT NOT NULL, amount REAL NOT NULL, period TEXT NOT NULL CHECK (period IN ('daily', 'weekly', 'monthly', 'yearly')), start_date DATE NOT NULL, end_date DATE, is_active BOOLEAN DEFAULT TRUE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (wallet_id) REFERENCES wallets(id) ON DELETE CASCADE, FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL ); -- Trips for trip mode CREATE TABLE IF NOT EXISTS trips ( id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), wallet_id TEXT NOT NULL, name TEXT NOT NULL, destination TEXT, start_date DATE NOT NULL, end_date DATE, budget REAL, currency TEXT DEFAULT 'MYR', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (wallet_id) REFERENCES wallets(id) ON DELETE CASCADE ); -- Settings for user preferences CREATE TABLE IF NOT EXISTS user_settings ( user_id TEXT PRIMARY KEY, default_currency TEXT DEFAULT 'MYR', theme TEXT DEFAULT 'light' CHECK (theme IN ('light', 'dark', 'system')), language TEXT DEFAULT 'en', notification_email BOOLEAN DEFAULT TRUE, notification_push BOOLEAN DEFAULT FALSE, quick_add_categories TEXT, dashboard_widgets TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); -- Create indexes for performance CREATE INDEX IF NOT EXISTS idx_transactions_wallet ON transactions(wallet_id); CREATE INDEX IF NOT EXISTS idx_transactions_user ON transactions(user_id); CREATE INDEX IF NOT EXISTS idx_transactions_date ON transactions(date); CREATE INDEX IF NOT EXISTS idx_transactions_category ON transactions(category_id); CREATE INDEX IF NOT EXISTS idx_wallet_members_user ON wallet_members(user_id); CREATE INDEX IF NOT EXISTS idx_invoices_user ON invoices(user_id); CREATE INDEX IF NOT EXISTS idx_invoices_status ON invoices(status); CREATE INDEX IF NOT EXISTS idx_invoices_due ON invoices(due_date); CREATE INDEX IF NOT EXISTS idx_recurring_active ON recurring_transactions(is_active, last_processed);