Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/config/custom-environment-variables.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"templateWeeklyAnalytics": "CROWD_SENDGRID_TEMPLATE_WEEKLY_ANALYTICS",
"templateIntegrationDone": "CROWD_SENDGRID_TEMPLATE_INTEGRATION_DONE",
"templateCsvExport": "CROWD_SENDGRID_TEMPLATE_CSV_EXPORT",
"templateEagleEyeDigest": "CROWD_SENDGRID_TEMPLATE_EAGLE_EYE_DIGEST",
"weeklyAnalyticsUnsubscribeGroupId": "CROWD_SENDGRID_WEEKLY_ANALYTICS_UNSUBSCRIBE_GROUP_ID"
},
"plans": {
Expand Down
55 changes: 55 additions & 0 deletions backend/src/bin/jobs/eagleEyeEmailDigestTicks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Op } from 'sequelize'
import moment from 'moment'
import SequelizeRepository from '../../database/repositories/sequelizeRepository'
import { CrowdJob } from '../../types/jobTypes'
import { sendNodeWorkerMessage } from '../../serverless/utils/nodeWorkerSQS'
import { NodeWorkerMessageType } from '../../serverless/types/workerTypes'
import { NodeWorkerMessageBase } from '../../types/mq/nodeWorkerMessageBase'

const job: CrowdJob = {
name: 'Eagle Eye Email Digest Ticker',
// every half hour
cronTime: '*/30 * * * *',
onTrigger: async () => {
const options = await SequelizeRepository.getDefaultIRepositoryOptions()
const users = (
await options.database.user.findAll({
where: {
[Op.and]: [
{
'eagleEyeSettings.emailDigestActive': {
[Op.ne]: null,
},
},
{
'eagleEyeSettings.emailDigestActive': {
[Op.eq]: true,
},
},
],
},
include: [
{
model: options.database.tenantUser,
as: 'tenants',
},
],
})
).filter(
(u) =>
u.eagleEyeSettings &&
u.eagleEyeSettings.emailDigestActive &&
moment() > moment(u.eagleEyeSettings.emailDigest.nextEmailAt),
)

for (const user of users) {
await sendNodeWorkerMessage(user.id, {
type: NodeWorkerMessageType.NODE_MICROSERVICE,
user: user.id,
service: 'eagle-eye-email-digest',
} as NodeWorkerMessageBase)
}
},
}

export default job
2 changes: 2 additions & 0 deletions backend/src/bin/jobs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import memberScoreCoordinator from './memberScoreCoordinator'
import checkSqsQueues from './checkSqsQueues'
import refreshMaterializedViews from './refreshMaterializedViews'
import downgradeExpiredPlans from './downgradeExpiredPlans'
import eagleEyeEmailDigestTicks from './eagleEyeEmailDigestTicks'

const jobs: CrowdJob[] = [
weeklyAnalyticsEmailsCoordinator,
Expand All @@ -13,6 +14,7 @@ const jobs: CrowdJob[] = [
checkSqsQueues,
refreshMaterializedViews,
downgradeExpiredPlans,
eagleEyeEmailDigestTicks,
]

export default jobs
11 changes: 8 additions & 3 deletions backend/src/bin/scripts/send-weekly-analytics-email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import { timeout } from '../../utils/timing'
import { sendNodeWorkerMessage } from '../../serverless/utils/nodeWorkerSQS'
import { NodeWorkerMessageType } from '../../serverless/types/workerTypes'
import { NodeWorkerMessageBase } from '../../types/mq/nodeWorkerMessageBase'
import WeeklyAnalyticsEmailsHistoryRepository from '../../database/repositories/weeklyAnalyticsEmailsHistoryRepository'
import RecurringEmailsHistoryRepository from '../../database/repositories/recurringEmailsHistoryRepository'
import { RecurringEmailType } from '../../types/recurringEmailsHistoryTypes'

const banner = fs.readFileSync(path.join(__dirname, 'banner.txt'), 'utf8')

Expand Down Expand Up @@ -55,12 +56,16 @@ if (parameters.help || !parameters.tenant) {
const options = await SequelizeRepository.getDefaultIRepositoryOptions()
const tenantIds = parameters.tenant.split(',')
const weekOfYear = moment().utc().startOf('isoWeek').subtract(7, 'days').isoWeek().toString()
const waeRepository = new WeeklyAnalyticsEmailsHistoryRepository(options)
const rehRepository = new RecurringEmailsHistoryRepository(options)

for (const tenantId of tenantIds) {
const tenant = await options.database.tenant.findByPk(tenantId)
const isEmailAlreadySent =
(await waeRepository.findByWeekOfYear(tenantId, weekOfYear)) !== null
(await rehRepository.findByWeekOfYear(
tenantId,
weekOfYear,
RecurringEmailType.WEEKLY_ANALYTICS,
)) !== null

if (!tenant) {
log.error({ tenantId }, 'Tenant not found! Skipping.')
Expand Down
1 change: 1 addition & 0 deletions backend/src/config/configTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ export interface SendgridConfiguration {
templateWeeklyAnalytics: string
templateIntegrationDone: string
templateCsvExport: string
templateEagleEyeDigest: string
weeklyAnalyticsUnsubscribeGroupId: string
}

Expand Down
1 change: 1 addition & 0 deletions backend/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ export const SENDGRID_CONFIG: SendgridConfiguration = KUBE_MODE
templateWeeklyAnalytics: process.env.SENDGRID_TEMPLATE_WEEKLY_ANALYTICS,
templateIntegrationDone: process.env.SENDGRID_TEMPLATE_INTEGRATION_DONE,
templateCsvExport: process.env.SENDGRID_TEMPLATE_CSV_EXPORT,
templateEagleEyeDigest: process.env.SENDGRID_TEMPLATE_EAGLE_EYE_DIGEST,
}

export const NETLIFY_CONFIG: NetlifyConfiguration = KUBE_MODE
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
ALTER TABLE public."weeklyAnalyticsEmailsHistory" RENAME TO "recurringEmailsHistory";

CREATE TYPE public."recurringEmailTypes_type" AS ENUM ('weekly-analytics', 'eagle-eye-digest');

ALTER TABLE "recurringEmailsHistory" ADD COLUMN "type" public."recurringEmailTypes_type";

UPDATE "recurringEmailsHistory" SET "type"='weekly-analytics';

ALTER TABLE "recurringEmailsHistory" ALTER COLUMN "type" SET NOT NULL;
ALTER TABLE "recurringEmailsHistory" ALTER COLUMN "weekOfYear" DROP NOT NULL;


Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { randomUUID } from 'crypto'

import SequelizeTestUtils from '../../utils/sequelizeTestUtils'
import { WeeklyAnalyticsEmailsHistoryData } from '../../../types/weeklyAnalyticsEmailsHistoryTypes'
import WeeklyAnalyticsEmailsHistoryRepository from '../weeklyAnalyticsEmailsHistoryRepository'
import {
RecurringEmailsHistoryData,
RecurringEmailType,
} from '../../../types/recurringEmailsHistoryTypes'
import RecurringEmailsHistoryRepository from '../recurringEmailsHistoryRepository'

const db = null

describe('WeeklyAnalyticsEmailsHistory tests', () => {
describe('RecurringEmailsHistory tests', () => {
beforeEach(async () => {
await SequelizeTestUtils.wipeDatabase(db)
})
Expand All @@ -18,18 +21,19 @@ describe('WeeklyAnalyticsEmailsHistory tests', () => {
})

describe('create method', () => {
it('Should create weekly analytics email history with given values', async () => {
it('Should create recurring email history with given values', async () => {
const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db)

const historyData: WeeklyAnalyticsEmailsHistoryData = {
const historyData: RecurringEmailsHistoryData = {
emailSentAt: '2023-01-02T00:00:00Z',
type: RecurringEmailType.WEEKLY_ANALYTICS,
emailSentTo: ['anil@crowd.dev', 'uros@crowd.dev'],
tenantId: mockIRepositoryOptions.currentTenant.id,
weekOfYear: '1',
}

const waeRepository = new WeeklyAnalyticsEmailsHistoryRepository(mockIRepositoryOptions)
const history = await waeRepository.create(historyData)
const rehRepository = new RecurringEmailsHistoryRepository(mockIRepositoryOptions)
const history = await rehRepository.create(historyData)

expect(new Date(historyData.emailSentAt)).toStrictEqual(history.emailSentAt)
expect(historyData.emailSentTo).toStrictEqual(history.emailSentTo)
Expand All @@ -40,40 +44,43 @@ describe('WeeklyAnalyticsEmailsHistory tests', () => {
it('Should throw an error when mandatory fields are missing', async () => {
const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db)

const waeRepository = new WeeklyAnalyticsEmailsHistoryRepository(mockIRepositoryOptions)
const rehRepository = new RecurringEmailsHistoryRepository(mockIRepositoryOptions)
await expect(() =>
waeRepository.create({
rehRepository.create({
emailSentAt: '2023-01-02T00:00:00Z',
emailSentTo: ['anil@crowd.dev', 'uros@crowd.dev'],
tenantId: mockIRepositoryOptions.currentTenant.id,
weekOfYear: undefined,
type: undefined,
}),
).rejects.toThrowError()

await expect(() =>
waeRepository.create({
rehRepository.create({
emailSentAt: undefined,
emailSentTo: ['anil@crowd.dev', 'uros@crowd.dev'],
tenantId: mockIRepositoryOptions.currentTenant.id,
weekOfYear: '1',
type: RecurringEmailType.WEEKLY_ANALYTICS,
}),
).rejects.toThrowError()

await expect(() =>
waeRepository.create({
rehRepository.create({
emailSentAt: '2023-01-02T00:00:00Z',
emailSentTo: undefined,
tenantId: mockIRepositoryOptions.currentTenant.id,
weekOfYear: '1',
type: RecurringEmailType.WEEKLY_ANALYTICS,
}),
).rejects.toThrowError()

await expect(() =>
waeRepository.create({
rehRepository.create({
emailSentAt: '2023-01-02T00:00:00Z',
emailSentTo: ['anil@crowd.dev', 'uros@crowd.dev'],
tenantId: undefined,
weekOfYear: '1',
type: RecurringEmailType.WEEKLY_ANALYTICS,
}),
).rejects.toThrowError()
})
Expand All @@ -83,26 +90,27 @@ describe('WeeklyAnalyticsEmailsHistory tests', () => {
it('Should find historical receipt by id', async () => {
const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db)

const historyData: WeeklyAnalyticsEmailsHistoryData = {
const historyData: RecurringEmailsHistoryData = {
emailSentAt: '2023-01-02T00:00:00Z',
emailSentTo: ['anil@crowd.dev', 'uros@crowd.dev'],
tenantId: mockIRepositoryOptions.currentTenant.id,
weekOfYear: '1',
type: RecurringEmailType.WEEKLY_ANALYTICS,
}

const waeRepository = new WeeklyAnalyticsEmailsHistoryRepository(mockIRepositoryOptions)
const rehRepository = new RecurringEmailsHistoryRepository(mockIRepositoryOptions)

const receiptCreated = await waeRepository.create(historyData)
const receiptFoundById = await waeRepository.findById(receiptCreated.id)
const receiptCreated = await rehRepository.create(historyData)
const receiptFoundById = await rehRepository.findById(receiptCreated.id)

expect(receiptFoundById).toStrictEqual(receiptCreated)
})

it('Should return null for non-existing receipt entry', async () => {
const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db)
const waeRepository = new WeeklyAnalyticsEmailsHistoryRepository(mockIRepositoryOptions)
const rehRepository = new RecurringEmailsHistoryRepository(mockIRepositoryOptions)

const cache = await waeRepository.findById(randomUUID())
const cache = await rehRepository.findById(randomUUID())
expect(cache).toBeNull()
})
})
Expand All @@ -111,29 +119,32 @@ describe('WeeklyAnalyticsEmailsHistory tests', () => {
it('Should find historical receipt by week of year', async () => {
const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db)

const historyData: WeeklyAnalyticsEmailsHistoryData = {
const historyData: RecurringEmailsHistoryData = {
emailSentAt: '2023-01-02T00:00:00Z',
emailSentTo: ['anil@crowd.dev', 'uros@crowd.dev'],
tenantId: mockIRepositoryOptions.currentTenant.id,
weekOfYear: '1',
type: RecurringEmailType.EAGLE_EYE_DIGEST,
}

const waeRepository = new WeeklyAnalyticsEmailsHistoryRepository(mockIRepositoryOptions)
const rehRepository = new RecurringEmailsHistoryRepository(mockIRepositoryOptions)

const receiptCreated = await waeRepository.create(historyData)
const receiptCreated = await rehRepository.create(historyData)

// should find recently created receipt
let receiptFound = await waeRepository.findByWeekOfYear(
let receiptFound = await rehRepository.findByWeekOfYear(
mockIRepositoryOptions.currentTenant.id,
'1',
RecurringEmailType.EAGLE_EYE_DIGEST,
)

expect(receiptCreated).toStrictEqual(receiptFound)

// shouldn't find any receipts
receiptFound = await waeRepository.findByWeekOfYear(
receiptFound = await rehRepository.findByWeekOfYear(
mockIRepositoryOptions.currentTenant.id,
'2',
RecurringEmailType.EAGLE_EYE_DIGEST,
)

expect(receiptFound).toBeNull()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { v4 as uuid } from 'uuid'
import { QueryTypes } from 'sequelize'
import { WeeklyAnalyticsEmailsHistoryData } from '../../types/weeklyAnalyticsEmailsHistoryTypes'
import {
RecurringEmailsHistoryData,
RecurringEmailType,
} from '../../types/recurringEmailsHistoryTypes'
import { IRepositoryOptions } from './IRepositoryOptions'
import { RepositoryBase } from './repositoryBase'

class WeeklyAnalyticsEmailsHistoryRepository extends RepositoryBase<
WeeklyAnalyticsEmailsHistoryData,
class RecurringEmailsHistoryRepository extends RepositoryBase<
RecurringEmailsHistoryData,
string,
WeeklyAnalyticsEmailsHistoryData,
RecurringEmailsHistoryData,
unknown,
unknown
> {
Expand All @@ -16,23 +19,24 @@ class WeeklyAnalyticsEmailsHistoryRepository extends RepositoryBase<
}

/**
* Inserts weekly analytics email history.
* @param data weekly emails historical data
* Inserts recurring emails receipt history.
* @param data recurring emails historical data
* @param options
* @returns
*/
async create(data: WeeklyAnalyticsEmailsHistoryData): Promise<WeeklyAnalyticsEmailsHistoryData> {
async create(data: RecurringEmailsHistoryData): Promise<RecurringEmailsHistoryData> {
const historyInserted = await this.options.database.sequelize.query(
`INSERT INTO "weeklyAnalyticsEmailsHistory" ("id", "tenantId", "weekOfYear", "emailSentAt", "emailSentTo")
`INSERT INTO "recurringEmailsHistory" ("id", "type", "tenantId", "weekOfYear", "emailSentAt", "emailSentTo")
VALUES
(:id, :tenantId, :weekOfYear, :emailSentAt, ARRAY[:emailSentTo])
(:id, :type, :tenantId, :weekOfYear, :emailSentAt, ARRAY[:emailSentTo])
RETURNING "id"
`,
{
replacements: {
id: uuid(),
type: data.type,
tenantId: data.tenantId,
weekOfYear: data.weekOfYear,
weekOfYear: data.weekOfYear || null,
emailSentAt: data.emailSentAt,
emailSentTo: data.emailSentTo,
},
Expand All @@ -55,17 +59,20 @@ class WeeklyAnalyticsEmailsHistoryRepository extends RepositoryBase<
async findByWeekOfYear(
tenantId: string,
weekOfYear: string,
): Promise<WeeklyAnalyticsEmailsHistoryData> {
type: RecurringEmailType,
): Promise<RecurringEmailsHistoryData> {
const records = await this.options.database.sequelize.query(
`SELECT *
FROM "weeklyAnalyticsEmailsHistory"
FROM "recurringEmailsHistory"
WHERE "tenantId" = :tenantId
AND "weekOfYear" = :weekOfYear;
AND "weekOfYear" = :weekOfYear
and "type" = :type;
`,
{
replacements: {
tenantId,
weekOfYear,
type,
},
type: QueryTypes.SELECT,
},
Expand All @@ -85,10 +92,10 @@ class WeeklyAnalyticsEmailsHistoryRepository extends RepositoryBase<
* @param options
* @returns
*/
async findById(id: string): Promise<WeeklyAnalyticsEmailsHistoryData> {
async findById(id: string): Promise<RecurringEmailsHistoryData> {
const records = await this.options.database.sequelize.query(
`SELECT *
FROM "weeklyAnalyticsEmailsHistory"
FROM "recurringEmailsHistory"
WHERE id = :id;
`,
{
Expand All @@ -107,4 +114,4 @@ class WeeklyAnalyticsEmailsHistoryRepository extends RepositoryBase<
}
}

export default WeeklyAnalyticsEmailsHistoryRepository
export default RecurringEmailsHistoryRepository
Loading