@tiledesk/tiledesk-server
Version:
The Tiledesk server module
606 lines (482 loc) • 22.9 kB
JavaScript
const { ceil, floor } = require('lodash');
const moment = require('moment');
let winston = require('../config/winston');
const requestEvent = require('../event/requestEvent');
const messageEvent = require('../event/messageEvent');
const emailEvent = require('../event/emailEvent');
// NEW
// const PLANS_LIST = {
// FREE_TRIAL: { requests: 3000, messages: 0, tokens: 250000, email: 200, chatbots: 20, kbs: 50 }, // same as PREMIUM
// SANDBOX: { requests: 200, messages: 0, tokens: 100000, email: 200, chatbots: 2, kbs: 50 },
// BASIC: { requests: 1000, messages: 0, tokens: 2000000, email: 200, chatbots: 10, kbs: 200},
// PREMIUM: { requests: 3000, messages: 0, tokens: 5000000, email: 200, chatbots: 20, kbs: 500},
// CUSTOM: { requests: 3000, messages: 0, tokens: 5000000, email: 200, chatbots: 20, kbs: 500}
// }
const PLANS_LIST = {
//FREE_TRIAL: { requests: 200, messages: 0, tokens: 100000, voice_duration: 0, email: 200, chatbots: 20, namespace: 3, kbs: 50 }, // same as PREMIUM
SANDBOX: { requests: 200, messages: 0, tokens: 100000, voice_duration: 0, email: 200, chatbots: 2, namespace: 1, kbs: 50 },
BASIC: { requests: 800, messages: 0, tokens: 2000000, voice_duration: 0, email: 200, chatbots: 5, namespace: 1, kbs: 150 },
PREMIUM: { requests: 3000, messages: 0, tokens: 5000000, voice_duration: 0, email: 200, chatbots: 20, namespace: 3, kbs: 300 },
TEAM: { requests: 5000, messages: 0, tokens: 10000000, voice_duration: 0, email: 200, chatbots: 50, namespace: 10, kbs: 1000 },
//CUSTOM: { requests: 5000, messages: 0, tokens: 10000000, voice_duration: 120000, email: 200, chatbots: 50, namespace: 10, kbs: 1000 },
// FROM MARCH 2025
FREE_TRIAL: { requests: 3000, messages: 0, tokens: 5000000, voice_duration: 120000, email: 200, chatbots: 5, namespace: 1, kbs: 50 }, // same as PRO
STARTER: { requests: 800, messages: 0, tokens: 2000000, voice_duration: 0, email: 200, chatbots: 5, namespace: 1, kbs: 150 },
PRO: { requests: 3000, messages: 0, tokens: 5000000, voice_duration: 0, email: 200, chatbots: 20, namespace: 3, kbs: 300 },
BUSINESS: { requests: 5000, messages: 0, tokens: 10000000, voice_duration: 0, email: 200, chatbots: 50, namespace: 10, kbs: 1000 },
CUSTOM: { requests: 5000, messages: 0, tokens: 10000000, voice_duration: 120000, email: 200, chatbots: 50, namespace: 10, kbs: 1000 }
}
const typesList = ['requests', 'messages', 'email', 'tokens', 'voice_duration', 'chatbots', 'kbs']
let quotes_enabled = true;
class QuoteManager {
constructor(config) {
if (!config) {
throw new Error('config is mandatory')
}
if (!config.tdCache) {
throw new Error('config.tdCache is mandatory')
}
this.tdCache = config.tdCache;
this.project;
}
// INCREMENT KEY SECTION - START
async incrementRequestsCount(project, request) {
this.project = project;
let key = await this.generateKey(request, 'requests');
winston.verbose("[QuoteManager] incrementRequestsCount key: " + key);
await this.tdCache.incr(key)
this.sendEmailIfQuotaExceeded(project, request, 'requests', key);
return key;
}
async incrementMessagesCount(project, message) {
this.project = project;
let key = await this.generateKey(message, 'messages');
winston.verbose("[QuoteManager] incrementMessagesCount key: " + key);
await this.tdCache.incr(key)
return key;
}
async incrementEmailCount(project, email) {
this.project = project;
let key = await this.generateKey(email, 'email');
winston.verbose("[QuoteManager] incrementEmailCount key: " + key);
await this.tdCache.incr(key)
this.sendEmailIfQuotaExceeded(project, email, 'email', key);
return key;
}
async incrementTokenCount(project, data) { // ?? cosa passo? il messaggio per vedere la data?
this.project = project;
let key = await this.generateKey(data, 'tokens');
winston.verbose("[QuoteManager] incrementTokenCount key: " + key);
if (quotes_enabled === false) {
winston.debug("QUOTES DISABLED - incrementTokenCount")
return key;
}
let tokens = data.tokens * data.multiplier;
await this.tdCache.incrbyfloat(key, tokens);
// await this.tdCache.incrby(key, tokens);
this.sendEmailIfQuotaExceeded(project, data, 'tokens', key);
return key;
}
async incrementVoiceDurationCount(project, request) {
this.project = project;
let key = await this.generateKey(request, 'voice_duration');
winston.verbose("[QuoteManager] incrementVoiceDurationCount key: " + key);
if (quotes_enabled === false) {
winston.debug("QUOTES DISABLED - incrementVoiceDurationCount")
return key;
}
if (request?.duration) {
let duration = Math.round(request.duration / 1000); // from ms to s
await this.tdCache.incrby(key, duration);
this.sendEmailIfQuotaExceeded(project, request, 'voice_duration', key);
}
}
// INCREMENT KEY SECTION - END
async generateKey(object, type) {
let objectDate = moment(object.createdAt);
let subscriptionDate;
if (this.project.isActiveSubscription === true) {
if (this.project.profile.subStart) {
subscriptionDate = moment(this.project.profile.subStart);
winston.debug("Subscription date from subStart: " + subscriptionDate.toISOString());
} else {
// it should never happen
winston.error("Error: quote manager - isActiveSubscription is true but subStart does not exists.")
}
} else {
if (this.project.profile.subEnd) {
subscriptionDate = moment(this.project.profile.subEnd);
winston.debug("Subscription date from subEnd: " + subscriptionDate.toISOString());
} else {
subscriptionDate = moment(this.project.createdAt);
winston.debug("Subscription date from project createdAt: " + subscriptionDate.toISOString());
}
}
// Calculate the difference in months between the object date and the subscription date
let diffInMonths = objectDate.diff(subscriptionDate, 'months');
winston.debug("diffInMonths: ", diffInMonths)
// Make a clone of the subscription date --> this operation could be avoided
// Get the renewal date adding diffInMonths. Moment.js manage automatically the less longer month.
// E.g. if subscription date is 31 jan the renewals will be, 28/29 feb, 31 mar, 30 apr, etc.
let renewalDate = subscriptionDate.clone().add(diffInMonths, 'months');
// Force the renewal date equal to the last day of the month --> this operation could be avoided
if (renewalDate.date() !== subscriptionDate.date()) {
renewalDate = renewalDate.endOf('month');
}
winston.debug("renewalDate: ", renewalDate)
return "quotes:" + type + ":" + this.project._id + ":" + renewalDate.format('M/D/YYYY');
// return "quotes:" + type + ":" + this.project._id + ":" + renewalDate.format('MM/DD/YYYY');
// return "quotes:" + type + ":" + this.project._id + ":" + renewalDate.toLocaleString();
}
// async _generateKey(object, type) {
// winston.debug("generateKey object ", object)
// winston.debug("generateKey type " + type)
// let subscriptionDate;
// if (this.project.isActiveSubscription === true) {
// if (this.project.profile.subStart) {
// subscriptionDate = this.project.profile.subStart;
// } else {
// // it should never happen
// winston.error("Error: quote manager - isActiveSubscription is true but subStart does not exists.")
// }
// } else {
// if (this.project.profile.subEnd) {
// subscriptionDate = this.project.profile.subEnd;
// } else {
// subscriptionDate = this.project.createdAt;
// }
// }
// let objectDate = object.createdAt;
// winston.debug("objectDate " + objectDate);
// // converts date in timestamps and transform from ms to s
// const objectDateTimestamp = ceil(objectDate.getTime() / 1000);
// const subscriptionDateTimestamp = ceil(subscriptionDate.getTime() / 1000);
// let ndays = (objectDateTimestamp - subscriptionDateTimestamp) / 86400; // 86400 is the number of seconds in 1 day
// let nmonths = floor(ndays / 30); // number of month to add to the initial subscription date;
// let date = new Date(subscriptionDate);
// date.setMonth(date.getMonth() + nmonths);
// return "quotes:" + type + ":" + this.project._id + ":" + date.toLocaleDateString();
// }
/**
* Get current quote for a single type (tokens or request or ...)
*/
async getCurrentQuote(project, object, type) {
this.project = project;
let key = await this.generateKey(object, type);
winston.verbose("[QuoteManager] getCurrentQuote key: " + key);
let quote = await this.tdCache.get(key);
return Number(quote);
}
/**
* Get quotes for all types (tokens and request and ...)
*/
async getAllQuotes(project, obj) {
this.project = project;
let quotes = {}
for (let type of typesList) {
let key = await this.generateKey(obj, type);
let quote = await this.tdCache.get(key);
quotes[type] = {
quote: Number(quote)
};
}
return quotes;
}
/**
* Perform a check on a single type.
* Returns TRUE if the limit is not reached --> operation can be performed
* Returns FALSE if the limit is reached --> operation can't be performed
*/
async checkQuote(project, object, type) {
winston.verbose("checkQuote type " + type);
if (quotes_enabled === false) {
winston.verbose("QUOTES DISABLED - checkQuote for type " + type);
return true;
}
this.project = project;
let limits = await this.getPlanLimits();
winston.verbose("limits for current plan: ", limits)
let quote = await this.getCurrentQuote(project, object, type);
winston.verbose("getCurrentQuote resp: " + quote)
if (quote == null) {
return true;
}
if (quote < limits[type]) {
return true;
} else {
return false;
}
}
async checkQuoteForAlert(project, object, type) {
if (quotes_enabled === false) {
winston.verbose("QUOTES DISABLED - checkQuote for type " + type);
return (null, null);
}
this.project = project;
let limits = await this.getPlanLimits();
winston.verbose("limits for current plan: ", limits)
let quote = await this.getCurrentQuote(project, object, type);
winston.verbose("getCurrentQuote resp: ", quote)
let data = {
limits: limits,
quote: quote
}
return data;
}
async sendEmailIfQuotaExceeded(project, object, type, key) {
let data = await this.checkQuoteForAlert(project, object, type);
let limits = data.limits;
let limit = data.limits[type];
let quote = data.quote;
const checkpoint = await this.percentageCalculator(limit, quote);
if (checkpoint == 0) {
return;
}
winston.verbose("checkpoint perc: ", checkpoint);
// Generate redis key
let nKey = key + ":notify:" + checkpoint;
let result = await this.tdCache.get(nKey);
if (!result) {
let allQuotes = await this.getAllQuotes(project, object);
let quotes = await this.generateQuotesObject(allQuotes, limits);
let data = {
id_project: project._id,
project_name: project.name,
type: type,
checkpoint: checkpoint,
quotes: quotes
}
emailEvent.emit('email.send.quote.checkpoint', data);
await this.tdCache.set(nKey, 'true', { EX: 2592000 }); //seconds in one month = 2592000
} else {
winston.verbose("Quota checkpoint reached email already sent.")
}
}
async percentageCalculator(limit, quote) {
let p = (quote / limit) * 100;
if (p >= 100) { return 100; }
if (p >= 95) { return 95; }
if (p >= 75) { return 75; }
if (p >= 50) { return 50; }
return 0;
}
async invalidateCheckpointKeys(project, obj) {
this.project = project;
winston.verbose("invalidateCheckpointKeys project " + project._id);
let requests_key = await this.generateKey(obj, 'requests');
let tokens_key = await this.generateKey(obj, 'tokens');
let email_key = await this.generateKey(obj, 'email');
let checkpoints = ['50', '75', '95', '100']
checkpoints.forEach(async (checkpoint) => {
let nrequests_key = requests_key + ":notify:" + checkpoint;
let ntokens_key = tokens_key + ":notify:" + checkpoint;
let nemail_key = email_key + ":notify:" + checkpoint;
winston.verbose("invalidateCheckpointKeys nrequests_key: " + nrequests_key);
winston.verbose("invalidateCheckpointKeys ntokens_key: " + ntokens_key);
winston.verbose("invalidateCheckpointKeys nemail_key: " + nemail_key);
this.tdCache.del(nrequests_key);
this.tdCache.del(ntokens_key);
this.tdCache.del(nemail_key);
return true;
})
}
async generateQuotesObject(quotes, limits) {
let quotes_obj = {
requests: {
quote: quotes.requests.quote,
perc: ((quotes.requests.quote / limits['requests']) * 100).toFixed(1)
},
tokens: {
quote: quotes.tokens.quote,
perc: ((quotes.tokens.quote / limits['tokens']) * 100).toFixed(1)
},
email: {
quote: quotes.email.quote,
perc: ((quotes.email.quote / limits['email']) * 100).toFixed(1)
}
}
return quotes_obj
}
async getPlanLimits(project) {
if (project) {
this.project = project
};
let limits;
const plan = this.project.profile.name;
if (this.project.profile.type === 'payment') {
if (this.project.isActiveSubscription === false) {
limits = PLANS_LIST.SANDBOX;
return limits;
}
switch (plan) {
case 'Starter':
limits = PLANS_LIST.STARTER
break;
case 'Pro':
limits = PLANS_LIST.PRO
break;
case 'Business':
limits = PLANS_LIST.BUSINESS
break;
case 'Basic':
limits = PLANS_LIST.BASIC;
break;
case 'Premium':
limits = PLANS_LIST.PREMIUM;
break;
case 'Team':
limits = PLANS_LIST.TEAM;
break;
case 'Custom':
limits = PLANS_LIST.CUSTOM;
break;
case 'Growth': // OLD PLAN
limits = PLANS_LIST.BASIC
break;
case 'Scale': // OLD PLAN
limits = PLANS_LIST.PREMIUM
break;
case 'Plus': // OLD PLAN
limits = PLANS_LIST.CUSTOM
break;
default:
limits = PLANS_LIST.FREE_TRIAL;
}
} else {
if (this.project.trialExpired === false) {
limits = PLANS_LIST.FREE_TRIAL
} else {
limits = PLANS_LIST.SANDBOX;
}
}
if (this.project?.profile?.quotes) {
let profile_quotes = this.project?.profile?.quotes;
const merged_quotes = Object.assign({}, limits, profile_quotes);
winston.verbose("Custom Limits: ", limits)
return merged_quotes;
} else {
winston.verbose("Default Limits: ", limits)
return limits;
}
}
async getCurrentSlot(project) {
let subscriptionDate;
if (project.isActiveSubscription === true) {
if (project.profile.subStart) {
subscriptionDate = moment(project.profile.subStart);
winston.debug("Subscription date from subStart: " + subscriptionDate.toISOString());
} else {
// it should never happen
winston.error("Error: quote manager - isActiveSubscription is true but subStart does not exists.")
}
} else {
if (project.profile.subEnd) {
subscriptionDate = moment(project.profile.subEnd);
winston.debug("Subscription date from subEnd: " + subscriptionDate.toISOString());
} else {
subscriptionDate = moment(project.createdAt);
winston.debug("Subscription date from project createdAt: " + subscriptionDate.toISOString());
}
}
let now = moment();
winston.debug("now: ", now);
let diffInMonths = now.diff(subscriptionDate, 'months');
winston.debug("diffInMonths: ", diffInMonths)
let renewalDate = subscriptionDate.clone().add(diffInMonths, 'months').startOf('day');
winston.debug("renewalDate: ", renewalDate)
let slotEnd = subscriptionDate.clone().add(diffInMonths + 1, 'month');
slotEnd.subtract(1, 'day').endOf('day')
winston.debug("slotEnd: ", slotEnd)
// let slot = {
// startDate: renewalDate.format('DD/MM/YYYY'),
// endDate: slotEnd.format('DD/MM/YYYY')
// }
let slot = {
startDate: renewalDate,
endDate: slotEnd
}
return slot;
}
start() {
winston.verbose('QuoteManager start');
if (process.env.QUOTES_ENABLED !== undefined) {
if (process.env.QUOTES_ENABLED === false || process.env.QUOTES_ENABLED === 'false') {
quotes_enabled = false;
}
}
winston.info("QUOTES ENABLED: " + quotes_enabled);
// TODO - Try to generalize to avoid repetition
let incrementEventHandler = (object) => { }
let checkEventHandler = (object) => { }
// REQUESTS EVENTS - START
// requestEvent.on('request.create.quote.before', async (payload) => {
// let result = await this.checkQuote(payload.project, payload.request, 'requests');
// if (result == true) {
// winston.info("Limit not reached - a request can be created")
// } else {
// winston.info("Requests limit reached for the current plan!")
// }
// return result;
// });
requestEvent.on('request.create.quote', async (payload) => {
if (quotes_enabled === true) {
winston.verbose("request.create.quote event catched");
let result = await this.incrementRequestsCount(payload.project, payload.request);
return result;
} else {
winston.verbose("QUOTES DISABLED - request.create.quote event")
}
})
requestEvent.on('request.close.quote', async (payload) => {
if (quotes_enabled === true) {
winston.verbose("request.close.quote event catched");
let result = await this.incrementVoiceDurationCount(payload.project, payload.request);
return result;
} else {
winston.verbose("QUOTES DISABLED - request.close.quote event")
}
})
// REQUESTS EVENTS - END
// MESSAGES EVENTS - START
// messageEvent.on('message.create.quote.before', async (payload) => {
// let result = await this.checkQuote(payload.project, payload.message, 'messages');
// if (result == true) {
// winston.info("Limit not reached - a message can be created")
// } else {
// winston.info("Messages limit reached for the current plan!")
// }
// return result;
// })
messageEvent.on('message.create.quote', async (payload) => {
if (quotes_enabled === true) {
winston.verbose("message.create.quote event catched");
let result = await this.incrementMessagesCount(payload.project, payload.message);
return result;
} else {
winston.verbose("QUOTES DISABLED - message.create.quote event")
}
})
// MESSAGES EVENTS - END
// EMAIL EVENTS - START - Warning! Can't be used for check quote
// emailEvent.on('email.send.before', async (payload) => {
// let result = await this.checkQuote(payload.project, payload.email, 'email');
// if (result == true) {
// winston.info("Limit not reached - a message can be created")
// } else {
// winston.info("Email limit reached for the current plan!")
// }
// return result;
// })
emailEvent.on('email.send.quote', async (payload) => {
if (quotes_enabled === true) {
winston.verbose("email.send event catched");
let result = await this.incrementEmailCount(payload.project, payload.email);
return result;
} else {
winston.verbose("QUOTES DISABLED - email.send event")
}
})
// EMAIL EVENTS - END
}
}
module.exports = { QuoteManager };