From 9f79bed918d39541c9830a94478c4bca55ab22bd Mon Sep 17 00:00:00 2001 From: valeriagrazzini Date: Fri, 23 Sep 2022 17:20:01 +0200 Subject: [PATCH 1/6] created job --- openapi.json | 2 +- src/jobs/remindersJob.ts | 73 ++++++ src/models/Account.ts | 1 + src/proxies/AccountProxy.ts | 11 + src/templates/email/reminder.ejs | 413 +++++++++++++++++++++++++++++++ src/util/agenda.ts | 5 +- 6 files changed, 503 insertions(+), 2 deletions(-) create mode 100644 src/jobs/remindersJob.ts create mode 100644 src/templates/email/reminder.ejs diff --git a/openapi.json b/openapi.json index a816da48..df622934 100644 --- a/openapi.json +++ b/openapi.json @@ -1,7 +1,7 @@ { "swagger": "2.0", "info": { - "version": "1.26.2", + "version": "1.26.4", "title": "THX API Specification", "description": "User guides are available at https://docs.thx.network." }, diff --git a/src/jobs/remindersJob.ts b/src/jobs/remindersJob.ts new file mode 100644 index 00000000..fe48dba1 --- /dev/null +++ b/src/jobs/remindersJob.ts @@ -0,0 +1,73 @@ +import ERC20 from '../models/ERC20'; +import { ERC721 } from '../models/ERC721'; +import MailService from '../services/MailService'; +import AccountProxy from '../proxies/AccountProxy'; +import ejs from 'ejs'; +import path from 'path'; +import { DASHBOARD_URL, API_URL } from '../config/secrets'; +import { AssetPool } from '../models/AssetPool'; +import { Withdrawal } from '../models/Withdrawal'; + +export const sendRemindersJob = async () => { + console.log('START REMINDER PROCESS'); + + const accounts = await AccountProxy.getActiveAccountsEmail(); + + const promises = accounts.map(async (account) => { + const now = Date.now(); + const maxInactivityDays = 7; + + if ((now - new Date(account.createdAt).getTime()) / (24 * 60 * 60 * 1000) < maxInactivityDays) { + console.log('ACCOUNT REGISTERED LESS THAN 7 DAYS AGO'); + return; + } + const subject = 'THX Reminder'; + let title: string, message: string; + + const numERC20s = await ERC20.count({ sub: account.id }); + const numERC721s = await ERC721.count({ sub: account.id }); + + if (numERC20s + numERC721s == 0) { + title = 'Account created but no Tokens or Collectibles'; + message = 'Email with tips on how to deploy assets'; + await sendEmail({ to: account.email, subject, title, message }); + console.log('SENT EMAIL TOKEN COLLECTIBLES'); + return; + } + + const pools = await AssetPool.find({ sub: account.id }); + if (pools.length == 0) { + title = 'Account and tokens / collectibles created but no pools to start using features'; + message = 'Email with tips on how to config pool'; + await sendEmail({ to: account.email, subject, title, message }); + console.log('SENT EMAIL POOLS'); + return; + } + + const numWithdrawals = await Withdrawal.count({ sub: account.id }); + if (numWithdrawals == 0) { + title = 'Pool created for tokens / collectibles but no withdrawals for that pool'; + message = 'Email with tips on how to config rewards etc'; + await sendEmail({ to: account.email, subject, title, message }); + console.log('SENT EMAIL WITHDRAWALS'); + } + }); + await Promise.all(promises); + console.log('END REMINDER PROCESS'); +}; + +async function sendEmail(data: { to: string; subject: string; title: string; message: string }) { + const html = await ejs.renderFile( + path.dirname(__dirname) + '/templates/email/reminder.ejs', + { + title: data.title, + message: data.message, + baseUrl: API_URL, + dashboardUrl: DASHBOARD_URL, + }, + { async: true }, + ); + + await MailService.send(data.to, data.subject, html); +} +//checkTokensAndCollectibles(); diff --git a/src/models/Account.ts b/src/models/Account.ts index dd46f5a1..32bdd1cf 100644 --- a/src/models/Account.ts +++ b/src/models/Account.ts @@ -10,6 +10,7 @@ export interface IAccount { spotify?: any; plan: AccountPlanType; email: string; + createdAt?: Date; } export interface ERC20Token { chainId: ChainId; diff --git a/src/proxies/AccountProxy.ts b/src/proxies/AccountProxy.ts index 82238a95..57709f9f 100644 --- a/src/proxies/AccountProxy.ts +++ b/src/proxies/AccountProxy.ts @@ -51,6 +51,17 @@ export default class AccountProxy { return data; } + static async getActiveAccountsEmail(): Promise { + const { data } = await authClient({ + method: 'GET', + url: `account/emails`, + headers: { + Authorization: await getAuthAccessToken(), + }, + }); + return data; + } + static async isEmailDuplicate(email: string) { try { await authClient({ diff --git a/src/templates/email/reminder.ejs b/src/templates/email/reminder.ejs new file mode 100644 index 00000000..0b72f3bc --- /dev/null +++ b/src/templates/email/reminder.ejs @@ -0,0 +1,413 @@ + + + + + + + Reset your Password + + + + + This is preheader text. Some clients will show this text as a preview. + + + + + + + + + + \ No newline at end of file diff --git a/src/util/agenda.ts b/src/util/agenda.ts index f4a0ed55..e8a0e1f8 100644 --- a/src/util/agenda.ts +++ b/src/util/agenda.ts @@ -3,7 +3,7 @@ import { Agenda } from 'agenda'; import { logger } from './logger'; // import { jobProcessTransactions } from '@/jobs/transactionProcessor'; import { generateRewardQRCodesJob } from '@/jobs/rewardQRcodesJob'; - +import { sendRemindersJob } from '@/jobs/remindersJob'; const agenda = new Agenda({ name: 'jobs', maxConcurrency: 1, @@ -13,9 +13,11 @@ const agenda = new Agenda({ const EVENT_REQUIRE_TRANSACTIONS = 'requireTransactions'; const EVENT_SEND_DOWNLOAD_QR_EMAIL = 'sendDownloadQrEmail'; +const EVENT_REMINDER_SENT = 'sendRemindersJob'; // agenda.define(EVENT_REQUIRE_TRANSACTIONS, jobProcessTransactions); agenda.define(EVENT_SEND_DOWNLOAD_QR_EMAIL, generateRewardQRCodesJob); +agenda.define(EVENT_REMINDER_SENT, sendRemindersJob); db.connection.once('open', async () => { agenda.mongo(db.connection.getClient().db(), 'jobs'); @@ -24,6 +26,7 @@ db.connection.once('open', async () => { // agenda.every('5 seconds', EVENT_REQUIRE_TRANSACTIONS); agenda.every('5 seconds', EVENT_SEND_DOWNLOAD_QR_EMAIL); + agenda.every('0 9 * * *', EVENT_REMINDER_SENT); logger.info('AgendaJS successfully started job processor'); }); From 0b9b599de7ab9cfce2909185a6ba6a5346ae10aa Mon Sep 17 00:00:00 2001 From: valeriagrazzini Date: Fri, 23 Sep 2022 17:33:19 +0200 Subject: [PATCH 2/6] fix tests --- src/proxies/AccountProxy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/proxies/AccountProxy.ts b/src/proxies/AccountProxy.ts index 57709f9f..b17e5f6f 100644 --- a/src/proxies/AccountProxy.ts +++ b/src/proxies/AccountProxy.ts @@ -54,7 +54,7 @@ export default class AccountProxy { static async getActiveAccountsEmail(): Promise { const { data } = await authClient({ method: 'GET', - url: `account/emails`, + url: 'account/emails', headers: { Authorization: await getAuthAccessToken(), }, From 2b2b48183dc3c6e58c35ee0d3303105365ea6f8c Mon Sep 17 00:00:00 2001 From: valeriagrazzini Date: Tue, 27 Sep 2022 11:56:02 +0200 Subject: [PATCH 3/6] set reminders based on the pool --- src/jobs/remindersJob.ts | 56 ++++++++++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/src/jobs/remindersJob.ts b/src/jobs/remindersJob.ts index fe48dba1..e93efe9a 100644 --- a/src/jobs/remindersJob.ts +++ b/src/jobs/remindersJob.ts @@ -7,22 +7,22 @@ import path from 'path'; import { DASHBOARD_URL, API_URL } from '../config/secrets'; import { AssetPool } from '../models/AssetPool'; import { Withdrawal } from '../models/Withdrawal'; +import { logger } from '../util/logger'; export const sendRemindersJob = async () => { - console.log('START REMINDER PROCESS'); - + const subject = 'THX Reminder'; + let title: string, message: string; const accounts = await AccountProxy.getActiveAccountsEmail(); - const promises = accounts.map(async (account) => { + // REMINDERS BASED ON THE ACCOUNT + const accountPromises = accounts.map(async (account) => { const now = Date.now(); const maxInactivityDays = 7; if ((now - new Date(account.createdAt).getTime()) / (24 * 60 * 60 * 1000) < maxInactivityDays) { - console.log('ACCOUNT REGISTERED LESS THAN 7 DAYS AGO'); + logger.info('ACCOUNT REGISTERED LESS THAN 7 DAYS AGO'); return; } - const subject = 'THX Reminder'; - let title: string, message: string; const numERC20s = await ERC20.count({ sub: account.id }); const numERC721s = await ERC721.count({ sub: account.id }); @@ -31,7 +31,7 @@ export const sendRemindersJob = async () => { title = 'Account created but no Tokens or Collectibles'; message = 'Email with tips on how to deploy assets'; await sendEmail({ to: account.email, subject, title, message }); - console.log('SENT EMAIL TOKEN COLLECTIBLES'); + logger.info('SENT EMAIL TOKEN COLLECTIBLES'); return; } @@ -40,20 +40,43 @@ export const sendRemindersJob = async () => { title = 'Account and tokens / collectibles created but no pools to start using features'; message = 'Email with tips on how to config pool'; await sendEmail({ to: account.email, subject, title, message }); - console.log('SENT EMAIL POOLS'); + logger.info('SENT EMAIL POOLS'); return; } + }); - const numWithdrawals = await Withdrawal.count({ sub: account.id }); - if (numWithdrawals == 0) { - title = 'Pool created for tokens / collectibles but no withdrawals for that pool'; - message = 'Email with tips on how to config rewards etc'; - await sendEmail({ to: account.email, subject, title, message }); - console.log('SENT EMAIL WITHDRAWALS'); + // REMINDERS BASED ON THE POOL + const pools = await AssetPool.find(); + const poolPromises = pools.map(async (pool) => { + const numWithdrawals = await Withdrawal.count({ poolId: pool._id }); + if (numWithdrawals > 0) { + return; + } + const account = await AccountProxy.getById(pool.sub); + if (!account) { + logger.error('POOL ACCOUNT NOT FOUND', { poolId: pool._id }); + return; + } + if (!account.active) { + logger.info('POOL ACCOUNT NOT ACTIVE'); + return; + } + if (!account.email) { + logger.error('ACCOUNT EMAIL NOT SET', { accountId: account._id }); + return; } + title = 'Pool created for tokens / collectibles but no withdrawals for that pool'; + message = 'Email with tips on how to config rewards etc'; + await sendEmail({ to: account.email, subject, title, message }); + logger.info('SENT EMAIL WITHDRAWALS'); }); - await Promise.all(promises); - console.log('END REMINDER PROCESS'); + try { + logger.info('START REMINDER JOB'); + await Promise.all([...accountPromises, ...poolPromises]); + logger.info('END REMINDER JOB'); + } catch (err) { + logger.error('ERROR on Reminders Job', err); + } }; async function sendEmail(data: { to: string; subject: string; title: string; message: string }) { @@ -70,4 +93,3 @@ async function sendEmail(data: { to: string; subject: string; title: string; mes await MailService.send(data.to, data.subject, html); } -//checkTokensAndCollectibles(); From 1f09b8e1070606cad90042e7d97f35367981bc33 Mon Sep 17 00:00:00 2001 From: valeriagrazzini Date: Tue, 27 Sep 2022 11:57:26 +0200 Subject: [PATCH 4/6] added comment --- src/util/agenda.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/agenda.ts b/src/util/agenda.ts index e8a0e1f8..50c546c5 100644 --- a/src/util/agenda.ts +++ b/src/util/agenda.ts @@ -26,7 +26,7 @@ db.connection.once('open', async () => { // agenda.every('5 seconds', EVENT_REQUIRE_TRANSACTIONS); agenda.every('5 seconds', EVENT_SEND_DOWNLOAD_QR_EMAIL); - agenda.every('0 9 * * *', EVENT_REMINDER_SENT); + agenda.every('0 9 * * *', EVENT_REMINDER_SENT); // EVERY DAY AT 9AM logger.info('AgendaJS successfully started job processor'); }); From 146a085dfe1cc8fda8ce57b5a04543ce93ccad95 Mon Sep 17 00:00:00 2001 From: valeriagrazzini Date: Wed, 5 Oct 2022 16:21:18 +0200 Subject: [PATCH 5/6] created schema TransactionalEmail --- src/models/TransactionalEmail.ts | 14 ++++++++++++++ src/types/TTransactionalEmail.ts | 4 ++++ src/util/agenda.ts | 3 --- 3 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 src/models/TransactionalEmail.ts create mode 100644 src/types/TTransactionalEmail.ts diff --git a/src/models/TransactionalEmail.ts b/src/models/TransactionalEmail.ts new file mode 100644 index 00000000..8c3ee60e --- /dev/null +++ b/src/models/TransactionalEmail.ts @@ -0,0 +1,14 @@ +import { TTransactionalEmail } from '@/types/TTransactionalEmail'; +import mongoose from 'mongoose'; + +export type TransactionalEmailDocument = mongoose.Document & TTransactionalEmail; + +const transactionalEmailSchema = new mongoose.Schema( + { + type: String, + sub: String, + }, + { timestamps: true }, +); + +export const Transaction = mongoose.model('TransactionalEmail', transactionalEmailSchema); diff --git a/src/types/TTransactionalEmail.ts b/src/types/TTransactionalEmail.ts new file mode 100644 index 00000000..6bacb1a8 --- /dev/null +++ b/src/types/TTransactionalEmail.ts @@ -0,0 +1,4 @@ +export type TTransactionalEmail = { + type: string; + sub: string; +}; diff --git a/src/util/agenda.ts b/src/util/agenda.ts index 9b09b948..5c11534d 100644 --- a/src/util/agenda.ts +++ b/src/util/agenda.ts @@ -14,11 +14,8 @@ const agenda = new Agenda({ const EVENT_UPDATE_PENDING_TRANSACTIONS = 'updatePendingTransactions'; const EVENT_SEND_DOWNLOAD_QR_EMAIL = 'sendDownloadQrEmail'; -<<<<<<< HEAD const EVENT_REMINDER_SENT = 'sendRemindersJob'; -======= const EVENT_SEND_DOWNLOAD_METADATA_QR_EMAIL = 'sendDownloadMetadataQrEmail'; ->>>>>>> origin/main agenda.define(EVENT_UPDATE_PENDING_TRANSACTIONS, updatePendingTransactions); agenda.define(EVENT_SEND_DOWNLOAD_QR_EMAIL, generateRewardQRCodesJob); From cf847bbf08be341b8bc77b639c163ad76205eca9 Mon Sep 17 00:00:00 2001 From: valeriagrazzini Date: Wed, 5 Oct 2022 20:26:16 +0200 Subject: [PATCH 6/6] created 3 different reminder jobs --- openapi.json | 12 ++- .../reminders/01_accountCreatedNoTokens.ts | 59 ++++++++++++ src/jobs/reminders/02_tokensCreatedNoPools.ts | 52 ++++++++++ .../reminders/03_poolsCreatedNoWithdrawals.ts | 61 ++++++++++++ src/jobs/reminders/utils.ts | 18 ++++ src/jobs/remindersJob.ts | 95 ------------------- src/models/TransactionalEmail.ts | 5 +- src/util/agenda.ts | 23 ++++- 8 files changed, 224 insertions(+), 101 deletions(-) create mode 100644 src/jobs/reminders/01_accountCreatedNoTokens.ts create mode 100644 src/jobs/reminders/02_tokensCreatedNoPools.ts create mode 100644 src/jobs/reminders/03_poolsCreatedNoWithdrawals.ts create mode 100644 src/jobs/reminders/utils.ts delete mode 100644 src/jobs/remindersJob.ts diff --git a/openapi.json b/openapi.json index 8fc14949..a6413f67 100644 --- a/openapi.json +++ b/openapi.json @@ -1,7 +1,7 @@ { "swagger": "2.0", "info": { - "version": "1.26.16", + "version": "1.26.18", "title": "THX API Specification", "description": "User guides are available at https://docs.thx.network." }, @@ -576,6 +576,11 @@ ], "description": "", "parameters": [ + { + "name": "forceSync", + "in": "query", + "type": "string" + }, { "name": "body", "in": "body", @@ -752,6 +757,11 @@ ], "description": "", "parameters": [ + { + "name": "forceSync", + "in": "query", + "type": "string" + }, { "name": "body", "in": "body", diff --git a/src/jobs/reminders/01_accountCreatedNoTokens.ts b/src/jobs/reminders/01_accountCreatedNoTokens.ts new file mode 100644 index 00000000..42fc8971 --- /dev/null +++ b/src/jobs/reminders/01_accountCreatedNoTokens.ts @@ -0,0 +1,59 @@ +import { TransactionalEmail } from '@/models/TransactionalEmail'; +import { EVENT_SEND_REMINDER_ACCOUNT_CREATED_NO_TOKENS, MAX_REMINDERS } from '@/util/agenda'; +import ERC20 from '../../models/ERC20'; +import { ERC721 } from '../../models/ERC721'; +import AccountProxy from '../../proxies/AccountProxy'; +import { logger } from '../../util/logger'; +import { sendEmail } from './utils'; + +export const reminderJobAccountCreatedNoTokens = async () => { + const reminderType = EVENT_SEND_REMINDER_ACCOUNT_CREATED_NO_TOKENS; + try { + logger.info(`START REMINDER JOB - ${reminderType}`); + + // Account created but no Tokens or Collectibles + const accounts = await AccountProxy.getActiveAccountsEmail(); + const promises = accounts.map(async (account) => { + const now = Date.now(); + const maxInactivityDays = 7; + + if ((now - new Date(account.createdAt).getTime()) / (24 * 60 * 60 * 1000) < maxInactivityDays) { + logger.info('ACCOUNT REGISTERED LESS THAN 7 DAYS AGO'); + return; + } + + const numERC20s = await ERC20.count({ sub: account.id }); + const numERC721s = await ERC721.count({ sub: account.id }); + + if (numERC20s + numERC721s == 0) { + const numEmailSent = await TransactionalEmail.count({ sub: account.id, type: reminderType }); + + if (numEmailSent >= MAX_REMINDERS) { + logger.info('MAXIMUM NUMBER OF EMAILS REACHED.'); + return; + } + + const subject = 'THX Reminder'; + const title = 'Account created but no Tokens or Collectibles'; + const message = 'Email with tips on how to deploy assets'; + + await sendEmail({ to: account.email, subject, title, message }); + + await TransactionalEmail.create({ + sub: account.id, + type: reminderType, + }); + logger.info('EMAIL SENT'); + } + }); + if (promises.length) { + await Promise.all(promises); + } else { + logger.info('NO ACCOUNTS TO PROCESS.'); + } + + logger.info('END REMINDER JOB'); + } catch (err) { + logger.error(`ERROR on Reminders Job - ${reminderType}`, err); + } +}; diff --git a/src/jobs/reminders/02_tokensCreatedNoPools.ts b/src/jobs/reminders/02_tokensCreatedNoPools.ts new file mode 100644 index 00000000..02fec4cd --- /dev/null +++ b/src/jobs/reminders/02_tokensCreatedNoPools.ts @@ -0,0 +1,52 @@ +import ERC20 from '../../models/ERC20'; +import { ERC721 } from '../../models/ERC721'; +import AccountProxy from '../../proxies/AccountProxy'; +import { AssetPool } from '../../models/AssetPool'; +import { logger } from '../../util/logger'; +import { sendEmail } from './utils'; +import { TransactionalEmail } from '@/models/TransactionalEmail'; +import { EVENT_SEND_REMINDER_TOKENS_CREATED_NO_POOLS, MAX_REMINDERS } from '@/util/agenda'; + +export const reminderJobTokensCreatedNoPools = async () => { + const reminderType = EVENT_SEND_REMINDER_TOKENS_CREATED_NO_POOLS; + try { + logger.info(`START REMINDER JOB - ${reminderType}`); + + // Account and tokens / collectibles created but no pools + const accounts = await AccountProxy.getActiveAccountsEmail(); + const promises = accounts.map(async (account) => { + const numERC20s = await ERC20.count({ sub: account.id }); + const numERC721s = await ERC721.count({ sub: account.id }); + + if (numERC20s + numERC721s == 0) { + return; + } + + const pools = await AssetPool.find({ sub: account.id }); + + if (pools.length == 0) { + const numEmailSent = await TransactionalEmail.count({ sub: account.id, type: reminderType }); + + if (numEmailSent >= MAX_REMINDERS) { + logger.info('MAXIMUM NUMBER OF EMAILS REACHED.'); + return; + } + + const subject = 'THX Reminder'; + const title = 'Account and tokens / collectibles created but no pools to start using features'; + const message = 'Email with tips on how to config pool'; + + await sendEmail({ to: account.email, subject, title, message }); + await TransactionalEmail.create({ sub: account.id, type: reminderType }); + logger.info('EMAIL SENT'); + } + }); + if (promises.length) { + await Promise.all(promises); + } else { + logger.info('NO ACCOUNTS TO PROCESS.'); + } + } catch (err) { + logger.error(`ERROR on Reminders Job - ${reminderType}`, err); + } +}; diff --git a/src/jobs/reminders/03_poolsCreatedNoWithdrawals.ts b/src/jobs/reminders/03_poolsCreatedNoWithdrawals.ts new file mode 100644 index 00000000..45944e53 --- /dev/null +++ b/src/jobs/reminders/03_poolsCreatedNoWithdrawals.ts @@ -0,0 +1,61 @@ +import AccountProxy from '../../proxies/AccountProxy'; +import { AssetPool } from '../../models/AssetPool'; +import { Withdrawal } from '../../models/Withdrawal'; +import { logger } from '../../util/logger'; +import { sendEmail } from './utils'; +import { TransactionalEmail } from '@/models/TransactionalEmail'; +import { EVENT_SEND_REMINDER_POOLS_CREATED_NO_WITHDRAWALS, MAX_REMINDERS } from '@/util/agenda'; + +export const reminderJobPoolsCreatedNoWithdrawals = async () => { + const reminderType = EVENT_SEND_REMINDER_POOLS_CREATED_NO_WITHDRAWALS; + try { + logger.info(`START REMINDER JOB - ${reminderType}`); + + // Pool created for tokens / collectibles but no withdrawals + const pools = await AssetPool.find(); + + const promises = pools.map(async (pool) => { + const numWithdrawals = await Withdrawal.count({ poolId: pool._id }); + if (numWithdrawals > 0) { + return; + } + const account = await AccountProxy.getById(pool.sub); + if (!account) { + logger.error('POOL ACCOUNT NOT FOUND', { poolId: pool._id }); + return; + } + if (!account.active) { + logger.info('POOL ACCOUNT NOT ACTIVE'); + return; + } + if (!account.email) { + logger.error('ACCOUNT EMAIL NOT SET', { accountId: account._id }); + return; + } + + const numEmailSent = await TransactionalEmail.count({ sub: account.id, type: reminderType }); + + if (numEmailSent >= MAX_REMINDERS) { + logger.info('MAXIMUM NUMBER OF EMAILS REACHED.'); + return; + } + + const subject = 'THX Reminder'; + const title = 'Pool created for tokens / collectibles but no withdrawals for that pool'; + const message = 'Email with tips on how to config rewards etc'; + + await sendEmail({ to: account.email, subject, title, message }); + await TransactionalEmail.create({ sub: account.id, type: reminderType }); + logger.info('EMAIL SENT'); + }); + + if (promises.length) { + await Promise.all(promises); + } else { + logger.info('NO POOLS TO PROCESS.'); + } + logger.info('END REMINDER JOB'); + } catch (err) { + logger.error(`ERROR on Reminders Job - ${reminderType}`, err); + } +}; diff --git a/src/jobs/reminders/utils.ts b/src/jobs/reminders/utils.ts new file mode 100644 index 00000000..09c7fe35 --- /dev/null +++ b/src/jobs/reminders/utils.ts @@ -0,0 +1,18 @@ +import MailService from '../../services/MailService'; +import ejs from 'ejs'; +import { API_URL, DASHBOARD_URL } from '@/config/secrets'; + +export async function sendEmail(data: { to: string; subject: string; title: string; message: string }) { + const html = await ejs.renderFile( + './src/templates/email/reminder.ejs', + { + title: data.title, + message: data.message, + baseUrl: API_URL, + dashboardUrl: DASHBOARD_URL, + }, + { async: true }, + ); + + return await MailService.send(data.to, data.subject, html); +} diff --git a/src/jobs/remindersJob.ts b/src/jobs/remindersJob.ts deleted file mode 100644 index e93efe9a..00000000 --- a/src/jobs/remindersJob.ts +++ /dev/null @@ -1,95 +0,0 @@ -import ERC20 from '../models/ERC20'; -import { ERC721 } from '../models/ERC721'; -import MailService from '../services/MailService'; -import AccountProxy from '../proxies/AccountProxy'; -import ejs from 'ejs'; -import path from 'path'; -import { DASHBOARD_URL, API_URL } from '../config/secrets'; -import { AssetPool } from '../models/AssetPool'; -import { Withdrawal } from '../models/Withdrawal'; -import { logger } from '../util/logger'; - -export const sendRemindersJob = async () => { - const subject = 'THX Reminder'; - let title: string, message: string; - const accounts = await AccountProxy.getActiveAccountsEmail(); - - // REMINDERS BASED ON THE ACCOUNT - const accountPromises = accounts.map(async (account) => { - const now = Date.now(); - const maxInactivityDays = 7; - - if ((now - new Date(account.createdAt).getTime()) / (24 * 60 * 60 * 1000) < maxInactivityDays) { - logger.info('ACCOUNT REGISTERED LESS THAN 7 DAYS AGO'); - return; - } - - const numERC20s = await ERC20.count({ sub: account.id }); - const numERC721s = await ERC721.count({ sub: account.id }); - - if (numERC20s + numERC721s == 0) { - title = 'Account created but no Tokens or Collectibles'; - message = 'Email with tips on how to deploy assets'; - await sendEmail({ to: account.email, subject, title, message }); - logger.info('SENT EMAIL TOKEN COLLECTIBLES'); - return; - } - - const pools = await AssetPool.find({ sub: account.id }); - if (pools.length == 0) { - title = 'Account and tokens / collectibles created but no pools to start using features'; - message = 'Email with tips on how to config pool'; - await sendEmail({ to: account.email, subject, title, message }); - logger.info('SENT EMAIL POOLS'); - return; - } - }); - - // REMINDERS BASED ON THE POOL - const pools = await AssetPool.find(); - const poolPromises = pools.map(async (pool) => { - const numWithdrawals = await Withdrawal.count({ poolId: pool._id }); - if (numWithdrawals > 0) { - return; - } - const account = await AccountProxy.getById(pool.sub); - if (!account) { - logger.error('POOL ACCOUNT NOT FOUND', { poolId: pool._id }); - return; - } - if (!account.active) { - logger.info('POOL ACCOUNT NOT ACTIVE'); - return; - } - if (!account.email) { - logger.error('ACCOUNT EMAIL NOT SET', { accountId: account._id }); - return; - } - title = 'Pool created for tokens / collectibles but no withdrawals for that pool'; - message = 'Email with tips on how to config rewards etc'; - await sendEmail({ to: account.email, subject, title, message }); - logger.info('SENT EMAIL WITHDRAWALS'); - }); - try { - logger.info('START REMINDER JOB'); - await Promise.all([...accountPromises, ...poolPromises]); - logger.info('END REMINDER JOB'); - } catch (err) { - logger.error('ERROR on Reminders Job', err); - } -}; - -async function sendEmail(data: { to: string; subject: string; title: string; message: string }) { - const html = await ejs.renderFile( - path.dirname(__dirname) + '/templates/email/reminder.ejs', - { - title: data.title, - message: data.message, - baseUrl: API_URL, - dashboardUrl: DASHBOARD_URL, - }, - { async: true }, - ); - - await MailService.send(data.to, data.subject, html); -} diff --git a/src/models/TransactionalEmail.ts b/src/models/TransactionalEmail.ts index 8c3ee60e..36ee5336 100644 --- a/src/models/TransactionalEmail.ts +++ b/src/models/TransactionalEmail.ts @@ -11,4 +11,7 @@ const transactionalEmailSchema = new mongoose.Schema( { timestamps: true }, ); -export const Transaction = mongoose.model('TransactionalEmail', transactionalEmailSchema); +export const TransactionalEmail = mongoose.model( + 'TransactionalEmail', + transactionalEmailSchema, +); diff --git a/src/util/agenda.ts b/src/util/agenda.ts index 5c11534d..668ee4bc 100644 --- a/src/util/agenda.ts +++ b/src/util/agenda.ts @@ -4,7 +4,9 @@ import { logger } from './logger'; import { updatePendingTransactions } from '@/jobs/updatePendingTransactions'; import { generateRewardQRCodesJob } from '@/jobs/rewardQRcodesJob'; import { generateMetadataRewardQRCodesJob } from '@/jobs/metadataRewardQRcodesJob'; -import { sendRemindersJob } from '@/jobs/remindersJob'; +import { reminderJobAccountCreatedNoTokens } from '@/jobs/reminders/01_accountCreatedNoTokens'; +import { reminderJobTokensCreatedNoPools } from '@/jobs/reminders/02_tokensCreatedNoPools'; +import { reminderJobPoolsCreatedNoWithdrawals } from '@/jobs/reminders/03_poolsCreatedNoWithdrawals'; const agenda = new Agenda({ name: 'jobs', maxConcurrency: 1, @@ -14,14 +16,21 @@ const agenda = new Agenda({ const EVENT_UPDATE_PENDING_TRANSACTIONS = 'updatePendingTransactions'; const EVENT_SEND_DOWNLOAD_QR_EMAIL = 'sendDownloadQrEmail'; -const EVENT_REMINDER_SENT = 'sendRemindersJob'; const EVENT_SEND_DOWNLOAD_METADATA_QR_EMAIL = 'sendDownloadMetadataQrEmail'; +const EVENT_SEND_REMINDER_ACCOUNT_CREATED_NO_TOKENS = 'reminder-job-account-created-no-tokens'; +const EVENT_SEND_REMINDER_TOKENS_CREATED_NO_POOLS = 'reminder-job-tokens-created-no-pools'; +const EVENT_SEND_REMINDER_POOLS_CREATED_NO_WITHDRAWALS = 'reminder-job-pools-created-no-withdrawals'; +const MAX_REMINDERS = 3; + agenda.define(EVENT_UPDATE_PENDING_TRANSACTIONS, updatePendingTransactions); agenda.define(EVENT_SEND_DOWNLOAD_QR_EMAIL, generateRewardQRCodesJob); -agenda.define(EVENT_REMINDER_SENT, sendRemindersJob); agenda.define(EVENT_SEND_DOWNLOAD_METADATA_QR_EMAIL, generateMetadataRewardQRCodesJob); +agenda.define(EVENT_SEND_REMINDER_ACCOUNT_CREATED_NO_TOKENS, reminderJobAccountCreatedNoTokens); +agenda.define(EVENT_SEND_REMINDER_TOKENS_CREATED_NO_POOLS, reminderJobTokensCreatedNoPools); +agenda.define(EVENT_SEND_REMINDER_POOLS_CREATED_NO_WITHDRAWALS, reminderJobPoolsCreatedNoWithdrawals); + db.connection.once('open', async () => { agenda.mongo(db.connection.getClient().db(), 'jobs'); @@ -30,7 +39,9 @@ db.connection.once('open', async () => { agenda.every('30 seconds', EVENT_UPDATE_PENDING_TRANSACTIONS); agenda.every('5 seconds', EVENT_SEND_DOWNLOAD_QR_EMAIL); agenda.every('5 seconds', EVENT_SEND_DOWNLOAD_METADATA_QR_EMAIL); - agenda.every('0 9 * * *', EVENT_REMINDER_SENT); // EVERY DAY AT 9AM + agenda.every('0 9 * * *', EVENT_SEND_REMINDER_ACCOUNT_CREATED_NO_TOKENS); // EVERY DAY AT 9 AM + agenda.every('5 9 * * *', EVENT_SEND_REMINDER_TOKENS_CREATED_NO_POOLS); // EVERY DAY AT 9.05 AM + agenda.every('10 9 * * *', EVENT_SEND_REMINDER_POOLS_CREATED_NO_WITHDRAWALS); // EVERY DAY AT 9.10 AM logger.info('AgendaJS successfully started job processor'); }); @@ -40,4 +51,8 @@ export { EVENT_UPDATE_PENDING_TRANSACTIONS, EVENT_SEND_DOWNLOAD_QR_EMAIL, EVENT_SEND_DOWNLOAD_METADATA_QR_EMAIL, + EVENT_SEND_REMINDER_ACCOUNT_CREATED_NO_TOKENS, + EVENT_SEND_REMINDER_TOKENS_CREATED_NO_POOLS, + EVENT_SEND_REMINDER_POOLS_CREATED_NO_WITHDRAWALS, + MAX_REMINDERS, };