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: () => {}, 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 const JSON_TYPE = 'JSON'; // --- 0. SETTINGS TABLE --- // Definizione completa per nuova installazione await connection.query(` CREATE TABLE IF NOT EXISTS settings ( id INT PRIMARY KEY, current_year INT, smtp_config ${JSON_TYPE}, features ${JSON_TYPE}, storage_config ${JSON_TYPE}, branding ${JSON_TYPE} ) `); // MIGRATION: Controllo e aggiunta colonne mancanti per installazioni esistenti try { let cols = []; if (DB_CLIENT === 'postgres') { const [res] = await connection.query("SELECT column_name FROM information_schema.columns WHERE table_name='settings'"); cols = res.map(c => c.column_name); } else { const [res] = await connection.query("SHOW COLUMNS FROM settings"); cols = res.map(c => c.Field); } if (!cols.includes('features')) { console.log('Migrating: Adding features to settings...'); await connection.query("ALTER TABLE settings ADD COLUMN features JSON"); } if (!cols.includes('storage_config')) { console.log('Migrating: Adding storage_config to settings...'); await connection.query("ALTER TABLE settings ADD COLUMN storage_config JSON"); } if (!cols.includes('branding')) { console.log('Migrating: Adding branding to settings...'); await connection.query("ALTER TABLE settings ADD COLUMN branding 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 condos try { let cols = []; if (DB_CLIENT === 'postgres') { const [res] = await connection.query("SELECT column_name FROM information_schema.columns WHERE table_name='condos'"); cols = res.map(c => c.column_name); } else { const [res] = await connection.query("SHOW COLUMNS FROM condos"); cols = res.map(c => c.Field); } if (!cols.includes('due_day')) { 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 ) `); // Migration families try { let cols = []; if (DB_CLIENT === 'postgres') { const [res] = await connection.query("SELECT column_name FROM information_schema.columns WHERE table_name='families'"); cols = res.map(c => c.column_name); } else { const [res] = await connection.query("SHOW COLUMNS FROM families"); cols = res.map(c => c.Field); } if (!cols.includes('custom_monthly_quota')) { console.log('Migrating: Adding custom_monthly_quota to families...'); await connection.query("ALTER TABLE families ADD COLUMN custom_monthly_quota DECIMAL(10, 2) NULL"); } } catch(e) { console.warn("Family migration warning:", e.message); } // --- 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_TYPE}, created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (condo_id) REFERENCES condos(id) ON DELETE CASCADE ) `); // Migration notices try { let cols = []; if (DB_CLIENT === 'postgres') { const [res] = await connection.query("SELECT column_name FROM information_schema.columns WHERE table_name='notices'"); cols = res.map(c => c.column_name); } else { const [res] = await connection.query("SHOW COLUMNS FROM notices"); cols = res.map(c => c.Field); } if (!cols.includes('target_families')) { console.log('Migrating: Adding target_families to notices...'); await connection.query("ALTER TABLE notices ADD COLUMN target_families JSON"); } } catch(e) { console.warn("Notices migration warning:", e.message); } // --- 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 ) `); // --- 13. DOCUMENTS (Cloud/Local) --- await connection.query(` CREATE TABLE IF NOT EXISTS documents ( id VARCHAR(36) PRIMARY KEY, condo_id VARCHAR(36) NOT NULL, title VARCHAR(255) NOT NULL, description TEXT, file_name VARCHAR(255) NOT NULL, file_type VARCHAR(100), file_size INT, tags ${JSON_TYPE}, storage_provider VARCHAR(50) DEFAULT 'local_db', file_data ${LONG_TEXT_TYPE}, upload_date ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (condo_id) REFERENCES condos(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, documents: true }; const defaultStorage = { provider: 'local_db', apiKey: '', apiSecret: '', bucket: '', region: '' }; const defaultBranding = { appName: 'CondoPay', primaryColor: 'blue', logoUrl: '', loginBackgroundUrl: '' }; if (rows.length === 0) { const currentYear = new Date().getFullYear(); await connection.query( 'INSERT INTO settings (id, current_year, features, storage_config, branding) VALUES (1, ?, ?, ?, ?)', [currentYear, JSON.stringify(defaultFeatures), JSON.stringify(defaultStorage), JSON.stringify(defaultBranding)] ); const hash = await bcrypt.hash('admin', 10); await connection.query( 'INSERT INTO users (id, email, password_hash, name, role, receive_alerts) VALUES (?, ?, ?, ?, ?, ?)', ['admin-id', 'admin@condo.it', hash, 'Amministratore', 'admin', true] ); console.log("Database initialized with seed data."); } if (DB_CLIENT !== 'postgres') { connection.release(); } } catch (e) { console.error("Database init error:", e); } }; module.exports = { pool: DB_CLIENT === 'postgres' ? pgPool : mysqlPool, initDb };