const { getAllContactDb } = require("../db/contact.db"); const { InsertNotificationErrorDb, updateNotificationErrorDb, getDeviceChannelReminder, getReminderNotificationErrorByYearlyDb, updateNotificationErrorByChanelReminderDb, } = require("../db/notification_error.db"); const { createNotificationErrorUserDb, updateNotificationErrorUserDb, getNotificationErrorByIdDb, } = require("../db/notification_error_user.db"); const { generateTokenRedirect, shortUrltiny, sendNotifikasi, } = require("../db/notification_wa.db"); const { getErrorCodeByBrandAndCodeDb } = require("../db/brand_code.db"); const { getDeviceNotificationByIdDb } = require("../db/notification_error.db"); const { exec } = require("child_process"); const util = require("util"); const execPromise = util.promisify(exec); const fs = require('fs').promises; const path = require("path"); const { getDeviceReminderChanelDb } = require("../db/device.db"); const baseDir = path.resolve(__dirname, '../scheduler'); const filePath = path.join(baseDir, 'reminder.json'); const filePathLog = path.join(baseDir, 'log.json'); class NotifikasiWaService { async saveReminder(data, isReset = false) { console.log(`Reminder proses dibuat`); try { let existing = []; // pastikan folder ada await fs.mkdir(path.dirname(filePath), { recursive: true }); try { const raw = await fs.readFile(filePath, 'utf-8'); if (raw) { existing = JSON.parse(raw); } } catch (err) { // file belum ada → buat file kosong await fs.writeFile(filePath, '[]', 'utf-8'); existing = []; } const now = new Date(); const newData = { ...data, time: now.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit', hour12: false, }), created_at: now.toISOString() // opsional (full timestamp) }; let finalData; if (isReset) { // kalau reset → replace semua if ( data && ( (Array.isArray(data) && data.length > 0) || (typeof data === 'object' && !Array.isArray(data) && Object.keys(data).length > 0) ) ) { finalData = data; } else { finalData = []; } } else { existing.push(newData); finalData = existing; } await fs.writeFile(filePath, JSON.stringify(finalData, null, 2), 'utf-8'); await this.saveLogReminder({ message: `Reminder berhasil disimpan`, data, }); console.log(`Reminder berhasil disimpan`); } catch (error) { console.log('Error saveReminder:', error); } } async saveLogReminder(data) { console.log(`Reminder Log dibuat`); try { let existing = []; // pastikan folder ada await fs.mkdir(path.dirname(filePathLog), { recursive: true }); try { const raw = await fs.readFile(filePathLog, 'utf-8'); if (raw) { existing = JSON.parse(raw); } } catch (err) { // file belum ada → buat file kosong await fs.writeFile(filePathLog, '[]', 'utf-8'); existing = []; } // 👉 selalu tambahkan waktu saat insert const now = new Date(); const newData = { ...data, time: now.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit', hour12: false, }), created_at: now.toISOString() // opsional (full timestamp) }; await existing.push(newData); await fs.writeFile(filePathLog, JSON.stringify(existing, null, 2), 'utf-8'); console.log(`Reminder Log berhasil disimpan`); } catch (error) { console.log('Error saveReminder:', error); } } async onNotification(topic, message) { try { const paramDb = { limit: 100, page: 1, criteria: "", active: 1, }; // const chanel = { // "time": "2025-12-11 11:10:58", // "c_4501": 4, // "c_5501": 3, // "c_6501": 0 // } if (topic === process.env.TOPIC_COD ?? "morek") { const dataMqtt = JSON.parse(message); const resultChanel = []; Object.entries(dataMqtt).forEach(([key, value]) => { if (key.startsWith("c_")) { resultChanel.push({ chanel_id: Number(key.slice(2)), value, }); } }); const results = await getAllContactDb(paramDb); const dataUsers = results.data; for (const chanel of resultChanel) { const deviceNotification = await getDeviceNotificationByIdDb( Number(chanel.chanel_id) ); const errorCode = await getErrorCodeByBrandAndCodeDb( deviceNotification?.brand_id ?? 0, chanel.value ); const data = { error_code_id: chanel.value, error_chanel: chanel.chanel_id, is_send: false, is_delivered: false, is_read: false, is_active: true, }; const resultNotificationError = await InsertNotificationErrorDb(data); let isSendNotification = false; for (const dataUser of dataUsers) { if (dataUser.is_active) { const tokenRedirect = await generateTokenRedirect( dataUser.contact_phone, dataUser.contact_name, resultNotificationError.notification_error_id ); const encodedToken = encodeURIComponent(tokenRedirect); const shortUrl = await shortUrltiny(encodedToken); const bodyMessage = `Hai ${dataUser.contact_name || "-"},\n` + `Terjadi peringatan dengan kode ${chanel?.value ?? "-"} "${errorCode?.error_code_name ?? "" }", Chanel ${chanel?.chanel_id ?? "-"} ` + `pada device ${deviceNotification?.device_name ?? "berikut"},` + `\nSilahkan cek detail pada link :` + `${shortUrl}`; const param = { idData: resultNotificationError.notification_error_id, userPhone: dataUser.contact_phone, userName: dataUser.contact_name, bodyMessage: bodyMessage, }; const resultNotificationErrorUser = await createNotificationErrorUserDb({ notification_error_id: param.idData, contact_phone: param.userPhone, contact_name: param.userName, message_error_issue: param.bodyMessage, is_send: false, }); const resultSend = await sendNotifikasi( param.userPhone, param.bodyMessage ); await updateNotificationErrorUserDb( resultNotificationErrorUser[0].notification_error_user_id, { is_send: resultSend.success, } ); if (resultSend.success) { isSendNotification = resultSend.success; } } } await updateNotificationErrorDb( resultNotificationError.notification_error_id, { is_send: isSendNotification, is_delivered: isSendNotification, } ); } } } catch (err) { return err; } } async loadReminder() { try { const raw = await fs.readFile(filePath, 'utf-8'); return raw ? JSON.parse(raw) : []; } catch (err) { return []; // file belum ada } } async deleteReminder(id) { const data = await this.loadReminder(); id = Number(id); const newData = data?.filter(item => item.notification_log !== id); // cek apakah ada yang terhapus if (data.length === newData.length) { console.log(`Reminder ${id} tidak ditemukan`); return false; } await this.saveReminder(newData, true); await this.saveLogReminder({ message: `Reminder ${id} berhasil dihapus` }) console.log(`Reminder ${id} berhasil dihapus`); return true; } async deleteReminderByChanelReminder(id) { let data = await this.loadReminder(); const newData = await data?.filter(item => item.error_chanel !== id); // cek apakah ada yang terhapus if (data.length === newData.length) { console.log(`Reminder ${id} tidak ditemukan`); return false; } await this.saveReminder(newData, true); await updateNotificationErrorByChanelReminderDb( Number(id), { is_active: false, } ); await this.saveLogReminder({ message: `Reminder ${id} berhasil dihapus` }) console.log(`Reminder ${id} berhasil dihapus`); return true; } async runReminder() { setInterval(async () => { let data = await this.loadReminder(); if (Array.isArray(data) && data.length > 0) { const now = new Date(); const newData = []; for (const item of data) { // skip kalau tidak aktif if (!item.active) continue; // skip kalau sudah max if (item.count >= item.max) continue; const [hour, minute] = item.start_at.split(':'); const start = new Date(); start.setHours(parseInt(hour), parseInt(minute), 0, 0); const lastRunDate = new Date(item.last_run); if (isNaN(lastRunDate.getTime())) { throw new Error(`Invalid last_run: ${item.last_run}`); } const nextRun = new Date( lastRunDate.getTime() + (item.interval * 60 * 60 * 1000) ); // format ke WIB (Jakarta) item.next_run = nextRun; item.next_run_indo = nextRun.toLocaleString('id-ID', { timeZone: 'Asia/Jakarta', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }); if (now >= nextRun) { const results = await getNotificationErrorByIdDb(item.notification_log); const dataUsers = results?.data ?? []; for (const dataUser of dataUsers) { const resultSend = await sendNotifikasi( dataUser.userPhone, dataUser.bodyMessage ); await this.saveLogReminder(resultSend); await updateNotificationErrorUserDb( dataUser.notification_error_user_id, { is_send: resultSend.success, } ); if (resultSend.success) { await updateNotificationErrorDb( item.notification_log, { is_send: true, is_delivered: true, } ); } } item.last_run = now.toISOString(); item.last_run_indo = now.toLocaleString('id-ID', { timeZone: 'Asia/Jakarta', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }); item.count += 1; } newData.push(item); } await this.saveReminder(newData, true); } }, 60 * 1000); } async onNotificationReminder(topic, message) { try { const paramDb = { limit: 100, page: 1, criteria: "", active: 1, }; // const chanel = { // "time": "2025-12-11 11:10:58", // "c_4501": 4, // "c_5501": 3, // "c_6501": 0 // } if (topic === process.env.TOPIC_COD_REMINDER ?? "morek") { const dataMqtt = JSON.parse(message); const resultChanel = []; Object.entries(dataMqtt).forEach(([key, value]) => { if (key.startsWith("c_")) { resultChanel.push({ chanel_id: Number(key.slice(2)), value, }); } }); // console.log('Reminder triggered A',resultChanel); for (const chanel of resultChanel) { const deviceReminder = await getDeviceReminderChanelDb( Number(chanel.chanel_id) ); if (deviceReminder?.[0]?.reminder_at) { const timeZone = 'Asia/Jakarta'; const now = new Date(); const year = now.getFullYear(); const reminderAt = new Date(deviceReminder[0].reminder_at); const deviceName = deviceReminder[0].device_name ?? '-'; // 🔹 ambil bulan & jam dalam timezone server const nowParts = new Intl.DateTimeFormat('en-US', { timeZone, month: 'numeric', hour: 'numeric', hour12: false, }).formatToParts(now); const reminderParts = new Intl.DateTimeFormat('en-US', { timeZone, month: 'numeric', hour: 'numeric', hour12: false, }).formatToParts(reminderAt); const timeReminderString = reminderAt.toLocaleTimeString('id-ID', { timeZone: 'Asia/Jakarta', hour: '2-digit', minute: '2-digit', hour12: false }).replace('.', ':'); // ganti titik jadi titik dua const getPart = (parts, type) => parseInt(parts.find(p => p.type === type).value, 10); const nowMonth = getPart(nowParts, 'month'); const nowHour = getPart(nowParts, 'hour'); const reminderMonth = getPart(reminderParts, 'month'); const reminderHour = getPart(reminderParts, 'hour'); // 🔥 logic utama const isTriggered = nowMonth > reminderMonth || (nowMonth === reminderMonth && nowHour >= reminderHour); if (isTriggered) { const valMqtt = chanel.value; const yearly = parseInt(valMqtt.match(/\d+/)?.[0], 10); if (!isNaN(yearly)) { const checkNotifExist = await getReminderNotificationErrorByYearlyDb(yearly ?? chanel.value, chanel.chanel_id, year); if (!checkNotifExist) { const data = { error_code_id: yearly ?? chanel.value, error_chanel: chanel.chanel_id, is_send: 0, is_delivered: 0, is_read: 0, is_active: 1, message_error_issue: `reminder ${chanel.value} in ${year}`, }; const resultNotificationError = await InsertNotificationErrorDb(data); await this.saveReminder({ notification_log: resultNotificationError.notification_error_id, error_code_id: data['error_code_id'], error_chanel: data['error_chanel'], start_at: timeReminderString, interval: 1, max: 3, active: 1, count: 0, last_run: now.toISOString(), last_run_indo: now.toLocaleString('id-ID', { timeZone: 'Asia/Jakarta', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }), next_run: null, next_run_indo: null }, false); const results = await getAllContactDb(paramDb); const dataUsers = results.data; let isSendNotification = false; for (const dataUser of dataUsers) { if (dataUser.is_active) { const tokenRedirect = await generateTokenRedirect( dataUser.contact_phone, dataUser.contact_name, resultNotificationError.notification_error_id ); const encodedToken = encodeURIComponent(tokenRedirect); const shortUrl = await shortUrltiny(encodedToken); const bodyMessage = `Hai ${dataUser.contact_name || "-"},\n` + `Diberitahukan bahwa terdapat sparepart pada device "${deviceName ?? "-"}" ` + `yang telah memasuki jadwal perawatan tahunan.\n` + `\nSilakan segera lakukan pengecekan dan perawatan untuk memastikan kinerja tetap optimal.` + `\nDetail sparepart dapat dilihat pada link berikut:\n${shortUrl}`; const param = { idData: resultNotificationError.notification_error_id, userPhone: dataUser.contact_phone, userName: dataUser.contact_name, bodyMessage: bodyMessage, }; const resultNotificationErrorUser = await createNotificationErrorUserDb({ notification_error_id: param.idData, contact_phone: param.userPhone, contact_name: param.userName, message_error_issue: param.bodyMessage, is_send: false, }); const resultSend = await sendNotifikasi( param.userPhone, param.bodyMessage ); await this.saveLogReminder({ message: `Reminder dijalankan`, resultSend }) await updateNotificationErrorUserDb( resultNotificationErrorUser[0].notification_error_user_id, { is_send: resultSend.success, } ); if (resultSend.success) { isSendNotification = resultSend.success; } } } await updateNotificationErrorDb( resultNotificationError.notification_error_id, { is_send: isSendNotification, is_delivered: isSendNotification, } ); } } } } } } } catch (err) { console.log('Error onNotificationReminder:', err); throw err; } } async restartWhatsapp() { try { const processName = "Whatsapp-API-notification" && "cod-whatsapp-notif"; const { stdout } = await execPromise("pm2 jlist"); const processes = JSON.parse(stdout); const waProcess = processes.find((p) => p.name === processName); if (!waProcess) throw new Error(`PM2 ${processName} not found`); const waProcessId = waProcess.pm_id; const pathDelete = waProcess.pm2_env.pm_cwd; // const execLogs = (cmd) => { // const p = exec(cmd); // p.stdout.pipe(process.stdout); // p.stderr.pipe(process.stderr); // return new Promise((res) => p.on("exit", res)); // }; // console.log(`stop proses id: ${waProcessId}`) await execPromise(`start powershell -Command "pm2 stop ${waProcessId}"`); const pathFolderDelete = [ path.join(pathDelete, ".wwebjs_auth"), path.join(pathDelete, ".wwebjs_cache"), ]; // console.log(`path: ${pathDelete}`); pathFolderDelete.forEach((dir) => { if (fs.existsSync(dir)) { // console.log(`path folder: ${dir}`); fs.rmSync(dir, { recursive: true, force: true }); } }); // console.log(`start proses id: ${waProcessId}`); await execPromise(`start powershell -Command "pm2 start ${waProcessId}"`); return { success: true, message: "WhatsApp has been restarted." }; } catch (err) { return err; } } } module.exports = new NotifikasiWaService();