292 lines
10 KiB
TypeScript
292 lines
10 KiB
TypeScript
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<boolean> {
|
|
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;
|