const mysql = require('mysql2/promise'); const { Pool } = require('pg'); const bcrypt = require('bcryptjs'); require('dotenv').config(); const DB_CLIENT = process.env.DB_CLIENT || 'mysql'; // 'mysql' or 'postgres' // DB Configuration const dbConfig = { host: process.env.DB_HOST || 'localhost', user: process.env.DB_USER || 'root', password: process.env.DB_PASS || '', database: process.env.DB_NAME || 'condominio', port: process.env.DB_PORT || (DB_CLIENT === 'postgres' ? 5432 : 3306), }; let mysqlPool = null; let pgPool = null; if (DB_CLIENT === 'postgres') { pgPool = new Pool(dbConfig); } else { mysqlPool = mysql.createPool({ ...dbConfig, waitForConnections: true, connectionLimit: 10, queueLimit: 0 }); } const executeQuery = async (sql, params = []) => { if (DB_CLIENT === 'postgres') { let paramIndex = 1; const pgSql = sql.replace(/\?/g, () => `$${paramIndex++}`); const result = await pgPool.query(pgSql, params); return [result.rows, result.fields]; } else { return await mysqlPool.query(sql, params); } }; const dbInterface = { query: executeQuery, getConnection: async () => { if (DB_CLIENT === 'postgres') { return { query: executeQuery, release: () => {}, // Mock transaction methods for Postgres simple wrapper // In a real prod app, you would get a specific client from the pool here beginTransaction: async () => {}, commit: async () => {}, rollback: async () => {} }; } else { return await mysqlPool.getConnection(); } } }; const initDb = async () => { try { const connection = await dbInterface.getConnection(); console.log(`Database connected successfully using ${DB_CLIENT}.`); const TIMESTAMP_TYPE = 'TIMESTAMP'; const LONG_TEXT_TYPE = DB_CLIENT === 'postgres' ? 'TEXT' : 'LONGTEXT'; // For base64 files // 0. Settings Table (Global App Settings) await connection.query(` CREATE TABLE IF NOT EXISTS settings ( id INT PRIMARY KEY, current_year INT, smtp_config JSON ${DB_CLIENT === 'postgres' ? 'NULL' : 'NULL'}, features JSON ${DB_CLIENT === 'postgres' ? 'NULL' : 'NULL'} ) `); // Migration: Add features column if not exists try { let hasFeatures = false; if (DB_CLIENT === 'postgres') { const [cols] = await connection.query("SELECT column_name FROM information_schema.columns WHERE table_name='settings'"); hasFeatures = cols.some(c => c.column_name === 'features'); } else { const [cols] = await connection.query("SHOW COLUMNS FROM settings"); hasFeatures = cols.some(c => c.Field === 'features'); } if (!hasFeatures) { console.log('Migrating: Adding features to settings...'); await connection.query("ALTER TABLE settings ADD COLUMN features JSON"); } } catch(e) { console.warn("Settings migration warning:", e.message); } // 1. Condos Table await connection.query(` CREATE TABLE IF NOT EXISTS condos ( id VARCHAR(36) PRIMARY KEY, name VARCHAR(255) NOT NULL, address VARCHAR(255), street_number VARCHAR(20), city VARCHAR(100), province VARCHAR(100), zip_code VARCHAR(20), notes TEXT, iban VARCHAR(50), paypal_client_id VARCHAR(255), default_monthly_quota DECIMAL(10, 2) DEFAULT 100.00, due_day INT DEFAULT 10, image VARCHAR(255), created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP ) `); // Migration for condos due_day try { let hasDueDay = false; if (DB_CLIENT === 'postgres') { const [cols] = await connection.query("SELECT column_name FROM information_schema.columns WHERE table_name='condos'"); hasDueDay = cols.some(c => c.column_name === 'due_day'); } else { const [cols] = await connection.query("SHOW COLUMNS FROM condos"); hasDueDay = cols.some(c => c.Field === 'due_day'); } if (!hasDueDay) { console.log('Migrating: Adding due_day to condos...'); await connection.query("ALTER TABLE condos ADD COLUMN due_day INT DEFAULT 10"); } } catch(e) { console.warn("Condo migration warning:", e.message); } // 2. Families Table await connection.query(` CREATE TABLE IF NOT EXISTS families ( id VARCHAR(36) PRIMARY KEY, condo_id VARCHAR(36), name VARCHAR(255) NOT NULL, unit_number VARCHAR(50), stair VARCHAR(50), floor VARCHAR(50), notes TEXT, contact_email VARCHAR(255), custom_monthly_quota DECIMAL(10, 2) NULL, created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (condo_id) REFERENCES condos(id) ON DELETE CASCADE ) `); // 3. Payments Table await connection.query(` CREATE TABLE IF NOT EXISTS payments ( id VARCHAR(36) PRIMARY KEY, family_id VARCHAR(36) NOT NULL, extraordinary_expense_id VARCHAR(36) NULL, amount DECIMAL(10, 2) NOT NULL, date_paid ${TIMESTAMP_TYPE} NOT NULL, for_month INT NOT NULL, for_year INT NOT NULL, notes TEXT, created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (family_id) REFERENCES families(id) ON DELETE CASCADE ) `); // 4. Users Table await connection.query(` CREATE TABLE IF NOT EXISTS users ( id VARCHAR(36) PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, name VARCHAR(255), role VARCHAR(20) DEFAULT 'user', phone VARCHAR(20), family_id VARCHAR(36) NULL, receive_alerts BOOLEAN DEFAULT TRUE, created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (family_id) REFERENCES families(id) ON DELETE SET NULL ) `); // 5. Alerts Table await connection.query(` CREATE TABLE IF NOT EXISTS alerts ( id VARCHAR(36) PRIMARY KEY, condo_id VARCHAR(36) NULL, subject VARCHAR(255) NOT NULL, body TEXT, days_offset INT DEFAULT 1, offset_type VARCHAR(50) DEFAULT 'before_next_month', send_hour INT DEFAULT 9, active BOOLEAN DEFAULT TRUE, last_sent ${TIMESTAMP_TYPE} NULL, created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (condo_id) REFERENCES condos(id) ON DELETE CASCADE ) `); // 6. Notices Table await connection.query(` CREATE TABLE IF NOT EXISTS notices ( id VARCHAR(36) PRIMARY KEY, condo_id VARCHAR(36) NOT NULL, title VARCHAR(255) NOT NULL, content TEXT, type VARCHAR(50) DEFAULT 'info', link VARCHAR(255), date ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP, active BOOLEAN DEFAULT TRUE, target_families JSON ${DB_CLIENT === 'postgres' ? 'NULL' : 'NULL'}, created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (condo_id) REFERENCES condos(id) ON DELETE CASCADE ) `); // 7. Notice Reads await connection.query(` CREATE TABLE IF NOT EXISTS notice_reads ( user_id VARCHAR(36), notice_id VARCHAR(36), read_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (user_id, notice_id), FOREIGN KEY (notice_id) REFERENCES notices(id) ON DELETE CASCADE ) `); // 8. Tickets Table await connection.query(` CREATE TABLE IF NOT EXISTS tickets ( id VARCHAR(36) PRIMARY KEY, condo_id VARCHAR(36) NOT NULL, user_id VARCHAR(36) NOT NULL, title VARCHAR(255) NOT NULL, description TEXT, status VARCHAR(20) DEFAULT 'OPEN', priority VARCHAR(20) DEFAULT 'MEDIUM', category VARCHAR(20) DEFAULT 'OTHER', created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP, updated_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (condo_id) REFERENCES condos(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) `); // 9. Ticket Attachments await connection.query(` CREATE TABLE IF NOT EXISTS ticket_attachments ( id VARCHAR(36) PRIMARY KEY, ticket_id VARCHAR(36) NOT NULL, file_name VARCHAR(255) NOT NULL, file_type VARCHAR(100), data ${LONG_TEXT_TYPE}, created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (ticket_id) REFERENCES tickets(id) ON DELETE CASCADE ) `); // 10. Ticket Comments await connection.query(` CREATE TABLE IF NOT EXISTS ticket_comments ( id VARCHAR(36) PRIMARY KEY, ticket_id VARCHAR(36) NOT NULL, user_id VARCHAR(36) NOT NULL, text TEXT NOT NULL, created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (ticket_id) REFERENCES tickets(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) `); // 11. Extraordinary Expenses await connection.query(` CREATE TABLE IF NOT EXISTS extraordinary_expenses ( id VARCHAR(36) PRIMARY KEY, condo_id VARCHAR(36) NOT NULL, title VARCHAR(255) NOT NULL, description TEXT, start_date ${TIMESTAMP_TYPE}, end_date ${TIMESTAMP_TYPE}, contractor_name VARCHAR(255), total_amount DECIMAL(15, 2) DEFAULT 0, created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (condo_id) REFERENCES condos(id) ON DELETE CASCADE ) `); await connection.query(` CREATE TABLE IF NOT EXISTS expense_items ( id VARCHAR(36) PRIMARY KEY, expense_id VARCHAR(36) NOT NULL, description VARCHAR(255), amount DECIMAL(15, 2) NOT NULL, FOREIGN KEY (expense_id) REFERENCES extraordinary_expenses(id) ON DELETE CASCADE ) `); await connection.query(` CREATE TABLE IF NOT EXISTS expense_shares ( id VARCHAR(36) PRIMARY KEY, expense_id VARCHAR(36) NOT NULL, family_id VARCHAR(36) NOT NULL, percentage DECIMAL(5, 2) NOT NULL, amount_due DECIMAL(15, 2) NOT NULL, amount_paid DECIMAL(15, 2) DEFAULT 0, status VARCHAR(20) DEFAULT 'UNPAID', FOREIGN KEY (expense_id) REFERENCES extraordinary_expenses(id) ON DELETE CASCADE, FOREIGN KEY (family_id) REFERENCES families(id) ON DELETE CASCADE ) `); await connection.query(` CREATE TABLE IF NOT EXISTS expense_attachments ( id VARCHAR(36) PRIMARY KEY, expense_id VARCHAR(36) NOT NULL, file_name VARCHAR(255) NOT NULL, file_type VARCHAR(100), data ${LONG_TEXT_TYPE}, created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (expense_id) REFERENCES extraordinary_expenses(id) ON DELETE CASCADE ) `); // 12. CONDO ORDINARY EXPENSES (USCITE) await connection.query(` CREATE TABLE IF NOT EXISTS condo_expenses ( id VARCHAR(36) PRIMARY KEY, condo_id VARCHAR(36) NOT NULL, description VARCHAR(255) NOT NULL, supplier_name VARCHAR(255), amount DECIMAL(10, 2) NOT NULL, payment_date ${TIMESTAMP_TYPE} NULL, status VARCHAR(20) DEFAULT 'UNPAID', payment_method VARCHAR(50), invoice_number VARCHAR(100), notes TEXT, created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (condo_id) REFERENCES condos(id) ON DELETE CASCADE ) `); await connection.query(` CREATE TABLE IF NOT EXISTS condo_expense_attachments ( id VARCHAR(36) PRIMARY KEY, condo_expense_id VARCHAR(36) NOT NULL, file_name VARCHAR(255) NOT NULL, file_type VARCHAR(100), data ${LONG_TEXT_TYPE}, created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (condo_expense_id) REFERENCES condo_expenses(id) ON DELETE CASCADE ) `); // --- SEEDING --- const [rows] = await connection.query('SELECT * FROM settings WHERE id = 1'); const defaultFeatures = { multiCondo: true, tickets: true, payPal: true, notices: true, reports: true, extraordinaryExpenses: true, condoFinancialsView: false }; if (rows.length === 0) { const currentYear = new Date().getFullYear(); await connection.query( 'INSERT INTO settings (id, current_year, features) VALUES (1, ?, ?)', [currentYear, JSON.stringify(defaultFeatures)] ); } else { // Ensure features column has defaults if null if (!rows[0].features) { await connection.query('UPDATE settings SET features = ? WHERE id = 1', [JSON.stringify(defaultFeatures)]); } } // ENSURE ADMIN EXISTS const [admins] = await connection.query('SELECT * FROM users WHERE email = ?', ['fcarra79@gmail.com']); if (admins.length === 0) { const hashedPassword = await bcrypt.hash('Mr10921.', 10); const { v4: uuidv4 } = require('uuid'); await connection.query( 'INSERT INTO users (id, email, password_hash, name, role) VALUES (?, ?, ?, ?, ?)', [uuidv4(), 'fcarra79@gmail.com', hashedPassword, 'Amministratore', 'admin'] ); } else { await connection.query('UPDATE users SET role = ? WHERE email = ?', ['admin', 'fcarra79@gmail.com']); } console.log('Database tables initialized.'); if (connection.release) connection.release(); } catch (error) { console.error('Database initialization failed:', error); process.exit(1); } }; module.exports = { pool: dbInterface, initDb };