zeitadler-terminplaner-backend/src/mail/notifyBeforeAppointment.ts

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;