import { PoolConnection } from 'mariadb'; import { GLOBAL_calendar_min_earliest_booking_time, notifyBeforeAppointment } from '../config'; import { getConnection as getConnectionDB } from '../database/initDatabase'; import getConnectionRabbit from './getConnection'; import MyEvents, { CalendarEvent } from '../database/reservedCustomer'; import redisClient from '../redis/init'; import MyRedisKeys from '../redis/keys'; import { getStore } from '../database/store'; import { getDateText } from './dateText'; import { getUser } from '../database/user'; import logger, { userLogger } from '../logger/logger'; async function checkForNotifications() { let conn: PoolConnection | null = null; try { conn = await getConnectionDB(); for (const notifyBefore of notifyBeforeAppointment) { const isFirstNotification = notifyBefore === notifyBeforeAppointment[0]; let startSoonTime = new Date(new Date().getTime() + 1000 * 60 * notifyBefore); const currentTime = new Date(); let rows; if (isFirstNotification) { rows = await conn.query( 'SELECT * FROM `calendar_customer` WHERE `verified_time` IS NOT NULL AND `send_next_notification_at` IS NULL AND `time_start` < ? AND `time_start` > ? ORDER BY RAND() LIMIT 1', [startSoonTime, currentTime] ); } else { rows = await conn.query( 'SELECT * FROM `calendar_customer` WHERE `verified_time` IS NOT NULL AND `send_next_notification_at` < ? AND `time_start` < ? AND `time_start` > ? ORDER BY RAND() LIMIT 1', [currentTime, startSoonTime, currentTime] ); } if (rows.length >= 1) { const row = rows[0]; console.log('rows', rows); const event: CalendarEvent = { store_id: row.store_id, customer_name: row.name, customer_email: row.email, customer_message: row.message ? row.message : undefined, customer_id: row.customer_id, calendar_event_id: row.calendar_event_id, user_id: row.user_id, activity_id: row.activity_id, time_start: row.time_start, time_end: row.time_end, customer_verified_time: row.verified_time ? row.verified_time : undefined, send_next_notification_at: row.send_next_notification_at ? row.send_next_notification_at : undefined, created_at: row.created_at, }; // redis mutex like lock await new Promise(async (resolve, reject) => { const redisLockKey = MyRedisKeys.appointmentNotificationLock(event.customer_id); const redisLockKeyTTL = 10; // 10 seconds let isRedisLocked = null; function lock() { return redisClient.set(redisLockKey, 'true', { EX: redisLockKeyTTL, NX: true }); } function refreshLock() { return redisClient.expire(redisLockKey, redisLockKeyTTL); } try { isRedisLocked = await lock(); } catch (error) { console.error(error); reject(error); return; } // if lock is not set if (isRedisLocked === null) { resolve(undefined); return; } if (new Date() > event.time_start) { resolve(undefined); return; } let latestNotificationTime = notifyBefore; for (let i = notifyBeforeAppointment.length - 1; i >= 0; i--) { const _notifyBefore = notifyBeforeAppointment[i]; const _startSoonTime = new Date(new Date().getTime() + 1000 * 60 * _notifyBefore); if (_startSoonTime > event.time_start) { latestNotificationTime = _notifyBefore; break; } } let latestNotificationTimeIndex = notifyBeforeAppointment.indexOf(latestNotificationTime); if (latestNotificationTimeIndex === -1) { console.error('latestNotificationTimeIndex === -1', event); resolve(undefined); return; } let nextStartSoonTime; if (latestNotificationTimeIndex + 1 >= notifyBeforeAppointment.length) { nextStartSoonTime = event.time_start; } else { nextStartSoonTime = new Date(event.time_start.getTime() - 1000 * 60 * notifyBeforeAppointment[latestNotificationTimeIndex + 1]); } if (conn) { await conn.query('UPDATE `calendar_customer` SET `send_next_notification_at` = ? WHERE `customer_id` = ?', [nextStartSoonTime, event.customer_id]); if (event.created_at <= new Date(new Date().getTime() - 1000 * 60 * GLOBAL_calendar_min_earliest_booking_time)) { try { await sendNotificationByEMail(event, latestNotificationTime); } catch (error) { userLogger.error( event.user_id, 'Customer Notification failed to send (begins ' + minutesToText('en', latestNotificationTime) + ')', JSON.stringify(event, null, 2), JSON.stringify(error, null, 2) ); } } } else { reject('conn is null'); return; } resolve(undefined); }); } } } catch (error) { console.error(error); logger.error('checkForNotifications()', JSON.stringify(error, null, 2)); } finally { if (conn) conn.end(); } } function minutesToText(lang: 'de' | 'en', minutes: number) { // convert minutes to text like: 1 Tag, 2 Stunden und 3 Minuten let text = ''; if (minutes < 0) minutes = 0; const days = Math.floor(minutes / 60 / 24); minutes -= days * 60 * 24; const hours = Math.floor(minutes / 60); minutes -= hours * 60; switch (lang) { case 'de': if (days > 0) { if (days === 1) text += 'einem'; else text += days; text += ' Tag'; if (days > 1) text += 'e'; } if (hours > 0) { if (text.length > 0) text += ', '; if (hours === 1) text += 'einer'; else text += hours; text += ' Stunde'; if (hours > 1) text += 'n'; } if (minutes > 0) { if (text.length > 0) text += ' und '; if (minutes === 1) text += 'einer'; else text += minutes; text += ' Minute'; if (minutes > 1) text += 'n'; } if (text.length === 0) text = '0 Minuten'; if (text === 'einem Tag') text = 'morgen'; else text = 'in ' + text; break; case 'en': if (days > 0) { if (days === 1) text += 'one'; else text += days; text += ' day'; if (days > 1) text += 's'; } if (hours > 0) { if (text.length > 0) text += ' and '; if (hours === 1) text += 'one'; else text += hours; text += ' hour'; if (hours > 1) text += 's'; } if (minutes > 0) { if (text.length > 0) text += ' and '; if (minutes === 1) text += 'one'; else text += minutes; text += ' minute'; if (minutes > 1) text += 's'; } if (text.length === 0) text = '0 minutes'; if (text === 'one day') text = 'tomorrow'; else text = 'in ' + text; break; default: text = minutes + ' minutes'; break; } return text; } async function cancelEmailNotifications(customer_id: string): Promise { let conn; try { conn = await getConnectionDB(); let rows = await conn.query('UPDATE `calendar_customer` SET `send_next_notification_at` = `time_end` WHERE `customer_id` = ?', [customer_id]); if (rows.affectedRows === 1) { return true; } else { return false; } } catch (error) { console.error(error); return false; } finally { if (conn) conn.end(); } } async function sendNotificationByEMail(event: CalendarEvent, inMinutes: number) { const queue = 'kk.mails'; const conn = await getConnectionRabbit(); // Sender const ch2 = await conn.createChannel(); const store = await getStore(event.store_id); const user = await getUser(event.user_id); let data = { m: event.customer_email, // UserMail t: 'embedEmailNotificationZeitAdler', // TemplateId l: 'de', // LanguageId // BodyData b: { name: event.customer_name, cancelURL: MyEvents.generateEMailCancelLink(event.customer_id), address: store.address, timeText: minutesToText('de', inMinutes), cancelNotifyURL: MyEvents.generateEMailCancelNotificationLink(event.customer_id), ...(await getDateText(event)), }, }; ch2.sendToQueue(queue, Buffer.from(JSON.stringify(data))); userLogger.info(event.user_id, 'Customer Notification sent (begins ' + minutesToText('en', inMinutes) + ') to ' + event.customer_email); } export { checkForNotifications, cancelEmailNotifications }; export default checkForNotifications;