diff --git a/db/device.db.js b/db/device.db.js index 0d2428c..4249874 100644 --- a/db/device.db.js +++ b/db/device.db.js @@ -149,12 +149,23 @@ const getDeviceReminderMonthlyDb = async (id) => { COALESCE(a.device_code, '') + ' - ' + COALESCE(a.device_name, '') AS device_code_name FROM m_device a LEFT JOIN m_brands b ON a.brand_id = b.brand_id - WHERE a.device_id = $1 AND a.deleted_at IS NULL AND a.reminder_at_monthly IS NOT NULL + WHERE a.device_id = $1 AND a.deleted_at IS NULL `; const result = await pool.query(queryText, [id]); return result.recordset; }; +const updateDeviceReminderMonthlyDb = async (deviceId, reminderMonthly) => { + const queryText = ` + UPDATE m_device + SET reminder_at_monthly = $1, + updated_at = CURRENT_TIMESTAMP + WHERE device_id = $2 AND deleted_at IS NULL + `; + await pool.query(queryText, [reminderMonthly, deviceId]); + return getDeviceByIdDb(deviceId); +}; + const getDeviceReminderCustomDb = async (id) => { const queryText = ` SELECT @@ -193,4 +204,5 @@ module.exports = { getDeviceReminderMonthlyDb, getDeviceReminderCustomDb, updateDeviceReminderCustomDb, + updateDeviceReminderMonthlyDb }; \ No newline at end of file diff --git a/routes/notifikasi-wa.route.js b/routes/notifikasi-wa.route.js index 3f40aaf..e75cc1b 100644 --- a/routes/notifikasi-wa.route.js +++ b/routes/notifikasi-wa.route.js @@ -11,10 +11,11 @@ router.post('/restart-wa', async (req, res) => { } }); -router.post('/reminder-sparepart/:id', async (req, res) => { +router.post('/reminder-monthly/:id', async (req, res) => { try { const { id } = req.params; - const result = await NotifikasiWaService.onMonthlyReminder(id); + const {reminder_at_monthly} = req.body + const result = await NotifikasiWaService.onMonthlyReminder(id, reminder_at_monthly); return res.status(200).json(result); } catch (error) { return res.status(500).json(error); diff --git a/scheduler/reminder.json b/scheduler/reminder.json index 0637a08..e69de29 100644 --- a/scheduler/reminder.json +++ b/scheduler/reminder.json @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/services/notifikasi-wa.service.js b/services/notifikasi-wa.service.js index 498aea5..13f5fbb 100644 --- a/services/notifikasi-wa.service.js +++ b/services/notifikasi-wa.service.js @@ -24,17 +24,25 @@ const { const { getErrorCodeByBrandAndCodeDb } = require("../db/brand_code.db"); +const { + getDeviceReminderChanelDb, + getDeviceReminderMonthlyDb, + getDeviceReminderCustomDb, + updateDeviceReminderCustomDb, + updateDeviceReminderMonthlyDb +} = require("../db/device.db"); + +const { + getSparepartsByMonthlyDb, + getSparepartsByCustomDb +} = require("../db/sparepart.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, - getDeviceReminderMonthlyDb, - getDeviceReminderCustomDb, - updateDeviceReminderCustomDb -} = require("../db/device.db"); -const { getSparepartsByMonthlyDb, getSparepartsByCustomDb } = require("../db/sparepart.db"); + const baseDir = path.resolve(__dirname, '../scheduler'); const filePath = path.join(baseDir, 'reminder.json'); // const filePathLog = path.join(baseDir, 'log.json'); @@ -605,8 +613,11 @@ class NotifikasiWaService { } } - async onMonthlyReminder(deviceId) { + + async onMonthlyReminder(deviceId, reminderMonthly) { try { + console.log(`🔄 Memulai onMonthlyReminder untuk device ID: ${deviceId}`); + const paramDb = { limit: 100, page: 1, @@ -614,154 +625,347 @@ class NotifikasiWaService { active: true, }; - const allDeviceReminders = await getDeviceReminderMonthlyDb(deviceId); + // 1. Validasi device + console.log(`📡 Mengambil data device dengan ID: ${deviceId}`); + const devices = await getDeviceReminderMonthlyDb(deviceId); - if (!allDeviceReminders || allDeviceReminders.length === 0) { - return { success: false, message: 'Tidak ada data device.' }; + if (!devices || devices.length === 0) { + console.error(`❌ Device dengan ID ${deviceId} tidak ditemukan`); + return { + success: false, + message: `Device dengan ID ${deviceId} tidak ditemukan di database` + }; } - const now = dayjs().tz(timeZone); - const currentYear = dayjs().tz(timeZone).year(); - const currentMonth = dayjs().tz(timeZone).month() + 1; + const device = devices[0]; + const deviceName = device.device_name; + const currentDeviceId = device.device_id; - for (const deviceReminder of allDeviceReminders) { + console.log(`✅ Device ditemukan: ${deviceName} (ID: ${currentDeviceId})`); - if (deviceReminder?.reminder_at_monthly) { - const deviceName = deviceReminder.device_name ?? '-'; - const currentDeviceId = deviceReminder.device_id; + // 2. Validasi dan update reminder_at_monthly jika diperlukan + let targetReminderTime = device.reminder_at_monthly; + console.log(`📅 Reminder time dari DB: ${targetReminderTime}`); - const reminderAt = dayjs.tz( - deviceReminder.reminder_at_monthly, - timeZone + if (reminderMonthly) { + console.log(`📝 Mengupdate reminder time ke: ${reminderMonthly}`); + const isValidDate = dayjs.tz(reminderMonthly, this.timeZone).isValid(); + if (!isValidDate) { + return { + success: false, + message: 'Format reminderMonthly tidak valid. Gunakan format YYYY-MM-DD HH:mm:ss' + }; + } + + const updatedDevice = await updateDeviceReminderMonthlyDb(deviceId, reminderMonthly); + if (!updatedDevice) { + return { + success: false, + message: 'Gagal mengupdate reminder monthly.' + }; + } + + targetReminderTime = reminderMonthly; + console.log(`✅ Reminder time berhasil diupdate ke: ${targetReminderTime}`); + } else if (!targetReminderTime) { + console.log(`⚠️ Tidak ada reminder time, setting default ke sekarang + 1 jam`); + const reminderTime = dayjs().tz(this.timeZone).add(1, 'hour').format('YYYY-MM-DD HH:mm:ss'); + const updatedDevice = await updateDeviceReminderMonthlyDb(deviceId, reminderTime); + if (!updatedDevice) { + return { + success: false, + message: 'Gagal mengupdate reminder monthly dengan default time.' + }; + } + targetReminderTime = reminderTime; + console.log(`✅ Default reminder time: ${targetReminderTime}`); + } + + console.log(`📅 Target reminder time: ${targetReminderTime}`); + + // 3. Parse target reminder time + const reminderAt = dayjs.tz(targetReminderTime, this.timeZone); + const now = dayjs().tz(this.timeZone); + + console.log(`🕐 Waktu sekarang: ${now.format('YYYY-MM-DD HH:mm:ss')}`); + console.log(`🕐 Waktu reminder: ${reminderAt.format('YYYY-MM-DD HH:mm:ss')}`); + + // 4. Jika sudah lewat atau sama, langsung kirim (sama seperti onCustomReminder) + const isEndDatePassed = now.isAfter(reminderAt) || now.isSame(reminderAt); + + if (isEndDatePassed) { + console.log(`⏰ Reminder time sudah terlewati, mengirim reminder sekarang untuk ${deviceName}`); + const sendResult = await this.sendMonthlyReminderNow( + currentDeviceId, + deviceName, + reminderAt + ); + return sendResult; + } + + // 5. Jika belum waktunya, jadwalkan cron job (sama seperti onCustomReminder) + console.log(`⏰ Menjadwalkan reminder untuk ${deviceName} pada ${reminderAt.format('YYYY-MM-DD HH:mm:ss')}`); + + // Stop cron job lama jika ada + this.stopCronJob(currentDeviceId); + + // Schedule cron job + const scheduleResult = await this.scheduleMonthlyReminderJob( + currentDeviceId, + deviceName, + reminderAt + ); + + return { + success: true, + message: `Reminder berhasil dijadwalkan untuk device ${deviceName}`, + schedule: scheduleResult, + nextRun: scheduleResult.nextRun + }; + + } catch (error) { + console.error('❌ Error onMonthlyReminder:', error); + console.error('❌ Stack trace:', error.stack); + return { + success: false, + message: 'Error pada onMonthlyReminder: ' + error.message, + error: error, + stack: error.stack + }; + } + } + + async scheduleMonthlyReminderJob(deviceId, deviceName, reminderAt) { + try { + const now = dayjs().tz(this.timeZone); + + const targetTime = reminderAt.clone() + .set('second', 0) + .set('millisecond', 0); + + const nextRun = targetTime.format('YYYY-MM-DD HH:mm:ss'); + + console.log(`Reminder untuk ${deviceName} dijadwalkan pada ${nextRun}`); + console.log(`Reminder At: ${reminderAt.format('YYYY-MM-DD HH:mm:ss')}`); + + + const timeoutId = setTimeout(async () => { + console.log(`Waktu pengiriman tiba untuk ${deviceName} pada ${dayjs().tz(this.timeZone).format('YYYY-MM-DD HH:mm:ss')}`); + try { + await this.sendMonthlyReminderNow(deviceId, deviceName, reminderAt); + } catch (error) { + console.error(`Gagal mengirim reminder untuk ${deviceName}:`, error); + } + }); + + this.cronJobs.set(deviceId, { + timeoutId, + nextRun, + reminderAt: reminderAt.format('YYYY-MM-DD HH:mm:ss') + }); + + const cronExpression = `${reminderAt.minute()} ${reminderAt.hour()} * * *`; + console.log(`Backup cron akan berjalan setiap hari pada ${reminderAt.hour()}:${reminderAt.minute()}`); + + const cronTask = cron.schedule(cronExpression, async () => { + console.log(`Backup cron job berjalan untuk ${deviceName} pada ${dayjs().tz(this.timeZone).format('YYYY-MM-DD HH:mm:ss')}`); + try { + const now = dayjs().tz(this.timeZone); + const isEndDatePassed = now.isAfter(reminderAt) || now.isSame(reminderAt); + + if (isEndDatePassed) { + const currentMonth = now.month() + 1; + const checkNotifExist = await getReminderNotificationErrorByMonthlyDb( + parseInt(`${currentMonth}`), + currentMonth, + deviceId + ); + + if (!checkNotifExist) { + await this.sendMonthlyReminderNow(deviceId, deviceName, reminderAt); + } else { + console.log(`Reminder untuk ${deviceName} sudah pernah dikirim`); + } + + cronTask.stop(); + this.cronJobs.delete(`cron_${deviceId}`); + } else { + console.log(`Menunggu reminder untuk ${deviceName}: ${reminderAt.format('YYYY-MM-DD HH:mm:ss')}`); + } + } catch (error) { + console.error(`Backup cron job gagal untuk ${deviceName}:`, error); + } + }, { + timezone: this.timeZone + }); + + this.cronJobs.set(`cron_${deviceId}`, { + cronTask, + nextRun, + reminderAt: reminderAt.format('YYYY-MM-DD HH:mm:ss') + }); + + return { + nextRun, + targetDate: targetTime.format('YYYY-MM-DD HH:mm:ss'), + reminderAt: reminderAt.format('YYYY-MM-DD HH:mm:ss'), + cronExpression: cronExpression, + }; + + } catch (error) { + console.error('Error scheduling monthly reminder:', error); + throw error; + } + } + + async sendMonthlyReminderNow(deviceId, deviceName, reminderAt) { + try { + console.log(`Mengirim reminder untuk ${deviceName}`); + + const paramDb = { + limit: 100, + page: 1, + criteria: "", + active: true, + }; + + const now = dayjs().tz(this.timeZone); + const currentMonth = now.month() + 1; + const currentYear = now.year(); + const monthlyCode = parseInt(`${currentMonth}`); + + const checkNotifExist = await getReminderNotificationErrorByMonthlyDb( + monthlyCode, + currentMonth, + deviceId + ); + + if (checkNotifExist) { + console.log(`Reminder untuk ${deviceName} bulan ${currentMonth} sudah pernah dibuat`); + return { + success: false, + message: `Reminder untuk device ${deviceName} sudah pernah dibuat.` + }; + } + + const spareparts = await getSparepartsByMonthlyDb(currentMonth); + const sparepartList = spareparts && spareparts.length > 0 + ? "\n\nDaftar sparepart yang perlu diperiksa:\n" + + spareparts.map((sp, idx) => `${idx + 1}. ${sp.sparepart_name || 'Sparepart'}`).join('\n') + : ""; + + const data = { + error_code_id: monthlyCode, + error_chanel: 0, + is_send: 0, + is_delivered: 0, + is_read: 0, + is_active: 1, + message_error_issue: `reminder monthly device ${deviceName} in month ${currentMonth}`, + }; + + const resultNotificationError = await InsertNotificationErrorDb(data); + if (!resultNotificationError) { + return { + success: false, + message: 'Gagal menyimpan notifikasi error' + }; + } + + const results = await getAllContactDb(paramDb); + const dataUsers = results.data; + + let isSendNotification = false; + let successCount = 0; + let failCount = 0; + + for (const dataUser of dataUsers) { + if (dataUser.is_active) { + const tokenRedirect = await generateTokenRedirect( + dataUser.contact_phone, + dataUser.contact_name, + resultNotificationError.notification_error_id ); - const nowKey = now.format('MM-DD HH:mm'); - const reminderKey = reminderAt.format('MM-DD HH:mm'); + const encodedToken = encodeURIComponent(tokenRedirect); + const shortUrl = await shortUrltiny(encodedToken); - const isTriggered = nowKey >= reminderKey; + const bodyMessage = + `Hai ${dataUser.contact_name || "-"},\n\n` + + `Diberitahukan bahwa terdapat sparepart pada device "${deviceName}" ` + + `yang telah memasuki jadwal perawatan bulanan untuk bulan ${currentMonth} pada tahun ${currentYear}.${sparepartList}\n\n` + + `Silakan segera lakukan pengecekan dan perawatan untuk memastikan kinerja tetap optimal.` - if (isTriggered) { - const monthlyCode = parseInt(`${currentMonth}`); + const param = { + idData: resultNotificationError.notification_error_id, + userPhone: dataUser.contact_phone, + userName: dataUser.contact_name, + bodyMessage: bodyMessage, + }; - const checkNotifExist = await getReminderNotificationErrorByMonthlyDb( - monthlyCode, - currentMonth, - currentDeviceId - ); + 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, + }); - if (checkNotifExist) { - throw new Error(`Reminder untuk device ${deviceName} device id ${currentDeviceId} bulan ${currentMonth} sudah pernah dibuat.`); - continue; - } + const resultSend = await sendNotifikasi( + param.userPhone, + param.bodyMessage + ); - const spareparts = await getSparepartsByMonthlyDb(currentMonth); - - let sparepartList = ""; - if (spareparts && spareparts.length > 0) { - sparepartList = "\n\nDaftar sparepart yang perlu diperiksa:\n" + - spareparts.map((sp, idx) => `${idx + 1}. ${sp.sparepart_name || 'Sparepart'}`).join('\n'); - } - - const data = { - error_code_id: monthlyCode, - error_chanel: 0, - is_send: 0, - is_delivered: 0, - is_read: 0, - is_active: 1, - message_error_issue: `reminder device ${deviceName} device_id: ${currentDeviceId} in month ${currentMonth}`, - }; - - const resultNotificationError = await InsertNotificationErrorDb(data); - 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\n` + - `Diberitahukan bahwa terdapat sparepart pada device "${deviceName}" ` + - `yang telah memasuki jadwal perawatan bulanan untuk bulan ${currentMonth} pada tahun ${currentYear}.${sparepartList}\n\n` + - `Silakan segera lakukan pengecekan dan perawatan untuk memastikan kinerja tetap optimal.`; - - 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, + if (resultNotificationErrorUser && resultNotificationErrorUser.length > 0) { + await updateNotificationErrorUserDb( + resultNotificationErrorUser[0].notification_error_user_id, { - is_send: isSendNotification, - is_delivered: isSendNotification, + is_send: resultSend.success, } ); + } - await this.saveReminder({ - notification_log: resultNotificationError.notification_error_id, - error_code_id: data['error_code_id'], - error_chanel: data['error_chanel'], - start_at: reminderAt.format('HH:mm'), - interval: 1, - max: 3, - active: 1, - count: 0, - last_run: now.toISOString(), - last_run_indo: now.format('DD-MM-YYYY HH:mm:ss'), - next_run: null, - next_run_indo: null, - message: `Reminder untuk ${deviceName} bulan ${currentMonth} telah dijalankan.`, - }, false); - + if (resultSend.success) { + isSendNotification = true; } } } - return { success: true, message: "Reminder has been sent." }; + await updateNotificationErrorDb( + resultNotificationError.notification_error_id, + { + is_send: isSendNotification, + is_delivered: isSendNotification, + } + ); - } catch (err) { - console.log('Error on Monthly Reminder:', err); - throw err; + await this.saveReminder({ + notification_log: resultNotificationError.notification_error_id, + error_code_id: data['error_code_id'], + error_chanel: data['error_chanel'], + start_at: now.format('HH:mm'), + interval: 1, + max: 3, + active: 1, + count: 0, + last_run: now.toISOString(), + last_run_indo: now.format('DD-MM-YYYY HH:mm:ss'), + next_run: null, + next_run_indo: null, + message: `Reminder monthly untuk ${deviceName} bulan ${currentMonth} telah dijalankan.`, + }, false); + + this.stopCronJob(deviceId); + + console.log(`Reminder berhasil dikirim untuk ${deviceName}`); + return { + success: true, + message: "Reminder has been sent.", + }; + + } catch (error) { + console.error('Error sending monthly reminder now:', error); + throw error; } }