const mysql = require('mysql2/promise'); // Create a connection pool const pool = mysql.createPool({ host: process.env.BILLING_DB_HOST, user: process.env.BILLING_DB_USER, password: process.env.BILLING_DB_PASS, database: process.env.BILLING_DB_NAME, waitForConnections: true, connectionLimit: 10, queueLimit: 0 }); async function getCustomerByPhone(phoneNumber) { try { // Normalize phone number for search // User sends e.g. "62812345678" or "0812345678" // Database has "no_wa" column which seems to vary (some have 62 prefix, some 08) based on customer.sql sample // We will try to match broad patterns. // If phoneNumber is 628..., we also try 08... // If phoneNumber is 08..., we also try 628... let searchNumbers = [phoneNumber]; let p = phoneNumber; // Clean @c.us suffix if (p.endsWith('@c.us')) p = p.replace('@c.us', ''); if (p.startsWith('62')) { searchNumbers.push('0' + p.substring(2)); } else if (p.startsWith('0')) { searchNumbers.push('62' + p.substring(1)); } // Add the cleaned number itself just in case if (!searchNumbers.includes(p)) searchNumbers.push(p); // Construct query const query = ` SELECT c.name, c.user_profile, c.c_status, c.due_date, c.address, c.no_services, pi.name as package_name, pi.price as package_price, pi.description as package_description FROM customer c LEFT JOIN services s ON c.no_services = s.no_services LEFT JOIN package_item pi ON s.item_id = pi.p_item_id WHERE c.no_wa IN (?) LIMIT 1 `; const [rows] = await pool.query(query, [searchNumbers]); if (rows.length > 0) { return rows[0]; } return null; } catch (error) { console.error("Database Error:", error); return null; // Fail gracefully } } // Test connection async function testConnection() { try { const connection = await pool.getConnection(); console.log("Database connected successfully!"); connection.release(); return true; } catch (error) { console.error("Database connection failed:", error.message); return false; } } async function searchCustomer(query, limit = 5) { try { const searchTerm = `%${query}%`; const sql = ` SELECT name, user_profile, c_status, due_date, address, no_services, no_wa FROM customer WHERE name LIKE ? OR no_wa LIKE ? OR c_status LIKE ? OR user_profile LIKE ? LIMIT ? `; const [rows] = await pool.query(sql, [searchTerm, searchTerm, searchTerm, searchTerm, limit]); return rows; } catch (error) { console.error("Database Search Error:", error); return []; } } async function getSummary() { try { const sql = `SELECT c_status, COUNT(*) as total FROM customer GROUP BY c_status`; const [rows] = await pool.query(sql); return rows; } catch (error) { console.error("DB Summary Error:", error); return []; } } // Safe Read-Only SQL execution for Admin AI async function executeReadOnlyQuery(sql) { try { // STRICT SECURITY CHECK // 1. Must start with SELECT // 2. Must not contain prohibited keywords (INSERT, UPDATE, DELETE, DROP, ALTER, TRUNCATE, REPLACE, CREATE, GRANT, REVOKE) // 3. Simple whitespace normalization for checking const normalizedSql = sql.trim().toUpperCase(); if (!normalizedSql.startsWith('SELECT')) { throw new Error("Security Violation: Only SELECT queries are allowed."); } const prohibited = ['INSERT', 'UPDATE', 'DELETE', 'DROP', 'ALTER', 'TRUNCATE', 'REPLACE', 'CREATE', 'GRANT', 'REVOKE', ';']; // Note: Semicolon check prevents multiple statement injection like "SELECT *; DROP TABLE..." for (const word of prohibited) { // Check for whole words to avoid false positives (e.g. "UPDATE_DATE" column might trigger "UPDATE") // A simple includes check might be too aggressive, but safer. // Let's use regex for word boundaries or just strict includes for dangerous keywords used as verbs // For now, strict check on prohibited keywords existing loosely if (normalizedSql.includes(word + ' ') || normalizedSql.includes(' ' + word) || normalizedSql.endsWith(word)) { // Basic heuristic // Better regex: const regex = new RegExp(`\\b${word}\\b`); if (regex.test(normalizedSql)) { throw new Error(`Security Violation: Keyword '${word}' is prohibited.`); } } } if (sql.includes(';')) { throw new Error("Security Violation: Multiple statements (;) are not allowed."); } const [rows] = await pool.query(sql); return rows; } catch (error) { console.error("SQL Exec Error:", error.message); return { error: error.message }; } } async function getSearchCount(query) { try { const searchTerm = `%${query}%`; const sql = ` SELECT COUNT(*) as total FROM customer WHERE name LIKE ? OR no_wa LIKE ? OR c_status LIKE ? OR user_profile LIKE ? OR address LIKE ? `; // Added 'address' to search capabilities as well per user request "jumlah pelanggan di alamat tertentu" const [rows] = await pool.query(sql, [searchTerm, searchTerm, searchTerm, searchTerm, searchTerm]); return rows[0].total; } catch (error) { console.error("DB Count Error:", error); return 0; } } module.exports = { getCustomerByPhone, searchCustomer, getSummary, getSearchCount, testConnection, executeReadOnlyQuery };