diff --git a/models/db/user.d.ts b/models/db/user.d.ts index d9ca1f521..1ca705a6b 100644 --- a/models/db/user.d.ts +++ b/models/db/user.d.ts @@ -32,6 +32,7 @@ interface User { utm_source?: string; // how the user found the site // virtual field - not stored in schema config?: UserConfig; + configs?: UserConfig[]; } export interface ReqUser extends User { diff --git a/pages/api/internal-jobs/email-digest/index.ts b/pages/api/internal-jobs/email-digest/index.ts index 08f0f4104..8ec393e2b 100644 --- a/pages/api/internal-jobs/email-digest/index.ts +++ b/pages/api/internal-jobs/email-digest/index.ts @@ -1,5 +1,6 @@ import * as aws from '@aws-sdk/client-ses'; import { defaultProvider } from '@aws-sdk/credential-provider-node'; +import { getStreakRankIndex, STREAK_RANK_GROUPS } from '@root/components/counters/AnimateCounterOne'; import DiscordChannel from '@root/constants/discordChannel'; import { GameId } from '@root/constants/GameId'; import { Games } from '@root/constants/Games'; @@ -10,6 +11,7 @@ import { getEnrichUserConfigPipelineStage } from '@root/helpers/enrich'; import { getGameFromId } from '@root/helpers/getGameIdFromReq'; import EmailLog from '@root/models/db/emailLog'; import { EnrichedLevel } from '@root/models/db/level'; +import UserConfig from '@root/models/db/userConfig'; import { convert } from 'html-to-text'; import { Types } from 'mongoose'; import { NextApiRequest, NextApiResponse } from 'next'; @@ -169,7 +171,7 @@ export async function sendEmailDigests(batchId: Types.ObjectId, limit: number) { from: UserConfigModel.collection.name, localField: '_id', foreignField: 'userId', - as: 'userConfig', + as: 'configs', }, }, // join email logs and get the last one @@ -328,11 +330,22 @@ export async function sendEmailDigests(batchId: Types.ObjectId, limit: number) { NewUserEmail = NewUserCampaign[daysRegistered]; } + let dailySubject = 'Levels of the Day - ' + todaysDatePretty; + + // get max config.calcCurrentStreak for all configs + const maxStreak = user.configs?.reduce((max, config) => Math.max(max, config.calcCurrentStreak || 0), 0); + + if (maxStreak && maxStreak > 0) { + const streakRank = STREAK_RANK_GROUPS[getStreakRankIndex(maxStreak)]; + + dailySubject = `Keep your streak of ${maxStreak} day${maxStreak === 1 ? '' : 's'} going! ${streakRank.emoji}`; + } + /* istanbul ignore next */ const EmailTextTable: { [key: string]: { title: string, message?: string, subject: string, featuredLevelsLabel: string } } = { [EmailType.EMAIL_DIGEST]: { title: `Welcome to your Thinky.gg daily digest for ${todaysDatePretty}.`, - subject: 'Levels of the Day - ' + todaysDatePretty, + subject: dailySubject, featuredLevelsLabel: 'Levels of the Day', }, [EmailType.EMAIL_7D_REACTIVATE]: { diff --git a/tests/pages/api/internal-jobs/email-digest.test.ts b/tests/pages/api/internal-jobs/email-digest.test.ts index 2222634aa..2cb05da1a 100644 --- a/tests/pages/api/internal-jobs/email-digest.test.ts +++ b/tests/pages/api/internal-jobs/email-digest.test.ts @@ -1,6 +1,7 @@ import { DEFAULT_GAME_ID } from '@root/constants/GameId'; import { NextApiRequestWrapper } from '@root/helpers/apiWrapper'; import { enableFetchMocks } from 'jest-fetch-mock'; +import MockDate from 'mockdate'; import { testApiHandler } from 'next-test-api-route-handler'; import { Logger } from 'winston'; import { EmailDigestSettingType, EmailType } from '../../../../constants/emailDigest'; @@ -314,6 +315,84 @@ describe('Email digest', () => { }, }); }, 10000); + + test('Run it with a user who has a streak', async () => { + // setup + await dbConnect(); + sendMailRefMock.ref = acceptMock; + jest.spyOn(logger, 'error').mockImplementation(() => ({} as Logger)); + jest.spyOn(logger, 'info').mockImplementation(() => ({} as Logger)); + jest.spyOn(logger, 'warn').mockImplementation(() => ({} as Logger)); + + const tomorrow = Date.now() + (1000 * 60 * 60 * 24 ); // Note... Date.now() here is being mocked each time too! + + MockDate.set(tomorrow); + + await Promise.all([ + UserModel.findByIdAndUpdate(TestId.USER, { + ts: Math.floor(Date.now() / 1000) - (30 * 24 * 60 * 60) // Set registration date to 30 days ago so get by all the intro emails + }), + EmailLogModel.deleteMany({ type: EmailType.EMAIL_DIGEST, userId: TestId.USER }), + UserConfigModel.findOneAndUpdate( + { userId: TestId.USER }, + { + calcCurrentStreak: 7, + lastPlayedAt: new Date(), + emailDigest: EmailDigestSettingType.DAILY + }, + { upsert: true } + ) + ]); + + // Update user config to have a streak + + await testApiHandler({ + pagesHandler: async (_, res) => { + const req: NextApiRequestWrapper = { + gameId: DEFAULT_GAME_ID, + method: 'GET', + query: { + secret: process.env.INTERNAL_JOB_TOKEN_SECRET_EMAILDIGEST + }, + body: {}, + headers: { + 'content-type': 'application/json', + }, + } as unknown as NextApiRequestWrapper; + + await handler(req, res); + }, + test: async ({ fetch }) => { + const res = await fetch(); + const response = await res.json(); + + console.log('done'); + expect(response.error).toBeUndefined(); + expect(res.status).toBe(200); + + // Check the email logs to verify the subject line includes the streak + const emailLogs = await EmailLogModel.find({ userId: TestId.USER }, {}, { sort: { createdAt: -1 } }); + const latestEmail = emailLogs[0]; + + expect(latestEmail).toBeDefined(); + expect(latestEmail.subject).toContain('Keep your streak of 7 days going!'); + expect(latestEmail.state).toBe(EmailState.SENT); + + expect(response.sent[EmailType.EMAIL_DIGEST]).toHaveLength(4); + + expect(response.failed[EmailType.EMAIL_DIGEST]).toHaveLength(0); + }, + }); + + // Clean up - reset the streak + await UserConfigModel.findOneAndUpdate( + { userId: TestId.USER }, + { + calcCurrentStreak: 0, + lastPlayedAt: null + } + ); + }, 10000); test('Running with a user with no userconfig', async () => { // delete user config await Promise.all([UserModel.findByIdAndDelete(TestId.USER),