/** * Invoice Module - Database * Database initialization and tables for invoice module */ import { query } from '@hykocx/zen/database'; /** * Check if a table exists in the database * @param {string} tableName - Name of the table to check * @returns {Promise} */ async function tableExists(tableName) { const result = await query( `SELECT EXISTS ( SELECT FROM information_schema.tables WHERE table_schema = 'public' AND table_name = $1 )`, [tableName] ); return result.rows[0].exists; } /** * Create all invoice-related tables (includes clients, items, and transactions) * @returns {Promise} */ export async function createTables() { const created = []; const skipped = []; // Import clients, items, categories, transactions and recurrences table creation const { createClientsTable } = await import('../clients/db.js'); const { createItemsTable } = await import('./items/db.js'); const { createCategoriesTable } = await import('./categories/db.js'); const { createTransactionsTable } = await import('./transactions/db.js'); const { createRecurrencesTable } = await import('./recurrences/db.js'); // Create clients table first (dependency for invoices) console.log('\n--- Clients (Invoice Module) ---'); const clientsResult = await createClientsTable(); if (clientsResult.created) created.push(clientsResult.tableName); else skipped.push(clientsResult.tableName); // Create categories table first (dependency for items) console.log('\n--- Categories (Invoice Module) ---'); const categoriesResult = await createCategoriesTable(); if (categoriesResult.created) created.push(categoriesResult.tableName); else skipped.push(categoriesResult.tableName); // Create items table (depends on categories) console.log('\n--- Items (Invoice Module) ---'); const itemsResult = await createItemsTable(); if (itemsResult.created) created.push(itemsResult.tableName); else skipped.push(itemsResult.tableName); // 1. Main invoices table const invoicesTableExists = await tableExists('zen_invoices'); if (!invoicesTableExists) { await query(` CREATE TABLE zen_invoices ( id SERIAL PRIMARY KEY, invoice_number VARCHAR(50) UNIQUE NOT NULL, token VARCHAR(64) UNIQUE NOT NULL, client_id INTEGER NOT NULL REFERENCES zen_clients(id) ON DELETE RESTRICT, issue_date DATE NOT NULL, due_date DATE NOT NULL, subtotal DECIMAL(10, 2) NOT NULL DEFAULT 0, tax_rate DECIMAL(5, 2) DEFAULT 0, tax_amount DECIMAL(10, 2) DEFAULT 0, total_amount DECIMAL(10, 2) NOT NULL, paid_amount DECIMAL(10, 2) DEFAULT 0, interest_amount DECIMAL(10, 2) DEFAULT 0, interest_last_calculated DATE, notes TEXT, status VARCHAR(50) DEFAULT 'draft', paid_at TIMESTAMPTZ, first_reminder_days INTEGER DEFAULT 30, is_recurring BOOLEAN DEFAULT false, recurring_frequency VARCHAR(50), recurring_end_date DATE, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ) `); await query(`CREATE INDEX idx_zen_invoices_invoice_number ON zen_invoices(invoice_number)`); await query(`CREATE INDEX idx_zen_invoices_token ON zen_invoices(token)`); await query(`CREATE INDEX idx_zen_invoices_client_id ON zen_invoices(client_id)`); await query(`CREATE INDEX idx_zen_invoices_status ON zen_invoices(status)`); await query(`CREATE INDEX idx_zen_invoices_due_date ON zen_invoices(due_date)`); await query(`CREATE INDEX idx_zen_invoices_interest_calc ON zen_invoices(status, due_date, interest_last_calculated) WHERE status IN ('sent', 'partial', 'overdue')`); created.push('zen_invoices'); console.log('✓ Created table: zen_invoices'); } else { skipped.push('zen_invoices'); console.log('- Table already exists: zen_invoices'); } // 2. Invoice items table const itemsTableExists = await tableExists('zen_invoice_items'); if (!itemsTableExists) { await query(` CREATE TABLE zen_invoice_items ( id SERIAL PRIMARY KEY, invoice_id INTEGER NOT NULL REFERENCES zen_invoices(id) ON DELETE CASCADE, name VARCHAR(255) NOT NULL, description TEXT, quantity DECIMAL(10, 2) NOT NULL, unit_price DECIMAL(10, 2) NOT NULL, total DECIMAL(10, 2) NOT NULL, sort_order INTEGER DEFAULT 0, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ) `); await query(`CREATE INDEX idx_zen_invoice_items_invoice_id ON zen_invoice_items(invoice_id)`); created.push('zen_invoice_items'); console.log('✓ Created table: zen_invoice_items'); } else { skipped.push('zen_invoice_items'); console.log('- Table already exists: zen_invoice_items'); } // 3. Invoice reminders table const remindersTableExists = await tableExists('zen_invoice_reminders'); if (!remindersTableExists) { await query(` CREATE TABLE zen_invoice_reminders ( id SERIAL PRIMARY KEY, invoice_id INTEGER NOT NULL REFERENCES zen_invoices(id) ON DELETE CASCADE, reminder_type VARCHAR(50) NOT NULL, days_before INTEGER NOT NULL, sent_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ) `); await query(`CREATE INDEX idx_zen_invoice_reminders_invoice_id ON zen_invoice_reminders(invoice_id)`); await query(`CREATE INDEX idx_zen_invoice_reminders_sent_at ON zen_invoice_reminders(sent_at)`); created.push('zen_invoice_reminders'); console.log('✓ Created table: zen_invoice_reminders'); } else { skipped.push('zen_invoice_reminders'); console.log('- Table already exists: zen_invoice_reminders'); } // 4. Interac credentials table const interacTableExists = await tableExists('zen_invoice_interac'); if (!interacTableExists) { await query(` CREATE TABLE zen_invoice_interac ( id SERIAL PRIMARY KEY, client_id INTEGER NOT NULL UNIQUE REFERENCES zen_clients(id) ON DELETE CASCADE, security_question VARCHAR(255) NOT NULL, security_answer VARCHAR(255) NOT NULL, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ) `); await query(`CREATE INDEX idx_zen_invoice_interac_client_id ON zen_invoice_interac(client_id)`); created.push('zen_invoice_interac'); console.log('✓ Created table: zen_invoice_interac'); } else { skipped.push('zen_invoice_interac'); console.log('- Table already exists: zen_invoice_interac'); } // 5. Transactions table (depends on invoices) console.log('\n--- Transactions (Invoice Module) ---'); const transactionsResult = await createTransactionsTable(); if (transactionsResult.created) created.push(transactionsResult.tableName); else skipped.push(transactionsResult.tableName); // 6. Recurrences table (depends on clients and invoices) console.log('\n--- Recurrences (Invoice Module) ---'); const recurrencesResult = await createRecurrencesTable(); if (recurrencesResult.created) created.push(recurrencesResult.tableName); else skipped.push(recurrencesResult.tableName); return { created, skipped }; } /** * Drop all invoice-related tables (includes clients, items, and transactions) * @returns {Promise} */ export async function dropTables() { // Import drop functions const { dropClientsTable } = await import('../clients/db.js'); const { dropItemsTable } = await import('./items/db.js'); const { dropCategoriesTable } = await import('./categories/db.js'); const { dropTransactionsTable } = await import('./transactions/db.js'); const { dropRecurrencesTable } = await import('./recurrences/db.js'); // Drop recurrences first (depends on invoices) await dropRecurrencesTable(); // Drop transactions (depends on invoices) await dropTransactionsTable(); // Drop invoice tables const tables = [ 'zen_invoice_reminders', 'zen_invoice_interac', 'zen_invoice_items', 'zen_invoices' ]; for (const tableName of tables) { const exists = await tableExists(tableName); if (exists) { await query(`DROP TABLE IF EXISTS ${tableName} CASCADE`); console.log(`✓ Dropped table: ${tableName}`); } } // Drop items (depends on categories) await dropItemsTable(); // Drop categories await dropCategoriesTable(); // Drop clients last await dropClientsTable(); } // Backward compatibility aliases export const createInvoiceTables = createTables; export const dropInvoiceTables = dropTables;