Skip to content

Commit

Permalink
assess life settings and reminder backend
Browse files Browse the repository at this point in the history
  • Loading branch information
RemcoSimonides committed Oct 24, 2023
1 parent 91d8f78 commit f193e17
Show file tree
Hide file tree
Showing 18 changed files with 534 additions and 150 deletions.
260 changes: 257 additions & 3 deletions apps/backend-functions/src/firestore/users/exercises/assess_life.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,266 @@
import { onDocumentCreate } from '@strive/api/firebase'
import { arrayUnion, logger, onDocumentCreate, onDocumentUpdate } from '@strive/api/firebase'
import { AssessLifeEntry, AssessLifeInterval, AssessLifeSettings, Message, createAssessLifeEntry, createAssessLifeSettings, createDearFutureSelf, createMessage } from '@strive/model'
import { getDocumentSnap, toDate, unique } from '../../../shared/utils'
import { addMonths, addQuarters, addWeeks, addYears, differenceInDays, formatISO, isBefore, isEqual, startOfMonth, startOfQuarter, startOfWeek } from 'date-fns'
import { getNextDay, startOfAssessLifeYear } from '@strive/exercises/assess-life/utils/date.utils'
import { deleteScheduledTask, upsertScheduledTask } from 'apps/backend-functions/src/shared/scheduled-task/scheduled-task'
import { ScheduledTaskUserExerciseAssessLife, enumWorkerType } from 'apps/backend-functions/src/shared/scheduled-task/scheduled-task.interface'

export const assessLifeCreatedHandler = onDocumentCreate(`Users/{uid}/Exercises/AssessLife`, 'assessLifeCreatedHandler',
export const assessLifeSettingsCreatedHandler = onDocumentCreate(`Users/{uid}/Exercises/AssessLife`, 'assessLifeSettingsCreatedHandler',
async (snapshot, context) => {

const { uid } = context.params as { uid: string }
const settings = createAssessLifeSettings(toDate({ ...snapshot.data(), id: snapshot.id }))

// if the preferred day is never, then do not send reminders
if (settings.preferredDay === 'never') return

return upsertReminder(uid, settings)
})

export const assessLifeSettingsChangeHandler = onDocumentUpdate(`Users/{uid}/Exercises/AssessLife`, 'assessLifeChangeHandler',
async (snapshot, context) => {

logger.log('changed assess life settings')

const { uid } = context.params as { uid: string }
const before = createAssessLifeSettings(toDate({ ...snapshot.before.data(), id: snapshot.id }))
const after = createAssessLifeSettings(toDate({ ...snapshot.after.data(), id: snapshot.id }))

logger.log('before', before)
logger.log('after', after)

const preferredDayChanged = before.preferredDay !== after.preferredDay
const preferredTimeChanged = before.preferredTime !== after.preferredTime

if (preferredDayChanged && after.preferredDay === 'never') {
logger.log('delete scheduled task')
return deleteScheduledTask(`${uid}assesslife`)
}

if (preferredDayChanged || preferredTimeChanged) {
logger.log('upsert reminder because of changed time')
return upsertReminder(uid, after)
}

const beforeAvailableIntervals = getAvailableIntervals(before)
const afterAvailableIntervals = getAvailableIntervals(after)

// check if all settings are never
if (beforeAvailableIntervals.length && !afterAvailableIntervals.length) {
logger.log('delete scheduled task because all intervals are never')
return deleteScheduledTask(`${uid}assesslife`)
}

// if any intervals have been added or if any intervals have been removed, update the reminder
const addedIntervals = afterAvailableIntervals.filter(interval => !beforeAvailableIntervals.includes(interval))
const removedIntervals = beforeAvailableIntervals.filter(interval => !afterAvailableIntervals.includes(interval))
logger.log('added intervals so updating', addedIntervals)
logger.log('removed intervals so updating', removedIntervals)
if (addedIntervals.length || removedIntervals.length) {
return upsertReminder(uid, after)
}
})

export const assessLifeEntryCreatedHandler = onDocumentCreate(`Users/{uid}/Exercises/AssessLife/Entries`, 'assessLifeCreatedHandler',
async (snapshot, context) => {

const { uid } = context.params
const entry = createAssessLifeEntry(toDate({ ...snapshot.data(), id: snapshot.id }))

const promises = Promise.all([
saveGratitude(uid, entry),
saveWheelOfLife(uid, entry),
saveDearFutureSelf(uid, entry),
saveImagine(uid, entry)
])

return promises

// TODO Sync goal priorities after submitting. And keep the goal priorities in the entry to see what the history was.

})

// export const assessLifeEntryChangeHandler = onDocumentCreate(`Users/{uid}/Exercises/AssessLife/Entries`, 'assessLifeChangeHandler',
// async (snapshot, context) => {

// const { uid } = context.params
// const entry = createAssessLifeEntry(toDate({ ...snapshot.data(), id: snapshot.id }))

// should not save entry when updating probably. Would have to update but that's getting complicated
// saveGratitude(uid, entry)

// TO DO
// Sync gratitude with gratitude journal
// Sync wheel of life with wheel of life
// Sync dear future self with dear future self
// Send imagine.future and imagine.die as dear future self message

})
// })

async function saveGratitude(uid: string, entry: AssessLifeEntry) {
const items = entry.gratitude.entries.slice(0, 3)
if (!items.length) return

const date = formatISO(new Date(), { representation: 'date' })
const snap = await getDocumentSnap(`Users/${uid}/Exercises/DailyGratitude/Entries/${date}`)
if (!snap.exists) await snap.ref.set({ items })
}

async function saveWheelOfLife(uid: string, entry: AssessLifeEntry) {
if (Object.values(entry.wheelOfLife).every(val => val === '')) return

const date = formatISO(new Date(), { representation: 'date' })
const snap = await getDocumentSnap(`Users/${uid}/Exercises/WheelOfLife/Entries/${date}`)
if (!snap.exists) await snap.ref.set({ ...entry.wheelOfLife })
}

async function saveDearFutureSelf(uid: string, entry: AssessLifeEntry) {
if (Object.values(entry.dearFutureSelf).every(val => val === '')) return

const deliveryDate = entry.interval === 'weekly' ? addWeeks(entry.createdAt, 1)
: entry.interval === 'monthly' ? addMonths(entry.createdAt, 1)
: entry.interval === 'quarterly' ? addQuarters(entry.createdAt, 1)
: addYears(entry.createdAt, 1)

const message1 = createMessage({
description: entry.dearFutureSelf.advice,
deliveryDate,
createdAt: entry.createdAt
})

const message2 = createMessage({
description: entry.dearFutureSelf.predictions,
deliveryDate,
createdAt: entry.createdAt
})

const message3 = createMessage({
description: entry.dearFutureSelf.anythingElse,
deliveryDate,
createdAt: entry.createdAt
})

addDearFutureSelfMessage(uid, message1)
addDearFutureSelfMessage(uid, message2)
addDearFutureSelfMessage(uid, message3)
}

async function saveImagine(uid: string, entry: AssessLifeEntry) {
if (Object.values(entry.imagine).every(val => val === '')) return

const deliveryDate = addYears(entry.createdAt, 5)

const message1 = createMessage({
description: entry.imagine.future,
deliveryDate,
createdAt: entry.createdAt
})

const message2 = createMessage({
description: entry.imagine.die,
deliveryDate,
createdAt: entry.createdAt
})

addDearFutureSelfMessage(uid, message1)
addDearFutureSelfMessage(uid, message2)
}

async function addDearFutureSelfMessage(uid: string, message: Message) {
const snap = await getDocumentSnap(`Users/${uid}/Exercises/DearFutureSelf`)
if (snap.exists) {
await snap.ref.update({
messages: arrayUnion(message)
})
} else {
const dfs = createDearFutureSelf({ id: 'DearFutureSelf', messages: [message] })
await snap.ref.set(dfs)
}
}

export function upsertReminder(uid: string, settings: AssessLifeSettings) {
const { performAt, performIntervals } = getNextReminder(settings)

const id = `${uid}assesslife`
const task: ScheduledTaskUserExerciseAssessLife = {
worker: enumWorkerType.userExerciseAssessLife,
performAt,
options: { userId: uid, intervals: performIntervals },
status: 'scheduled'
}

return upsertScheduledTask(id, task)
}

export function getNextReminder(settings: AssessLifeSettings) {
if (settings.preferredDay === 'never') throw new Error('Should not set reminders if preferred day is never')

const intervals = getAvailableIntervals(settings)
const now = settings.createdAt

const startOfNextInterval = {
weekly: (date: Date) => startOfWeek(addWeeks(date, 1)),
monthly: (date: Date) => startOfMonth(addMonths(date, 1)),
quarterly: (date: Date) => startOfQuarter(addQuarters(date, 1)),
yearly: (date: Date) => startOfAssessLifeYear(addYears(date, 1), 12, 24)
}

const startOfNextNextInterval = {
weekly: (date: Date) => startOfWeek(addWeeks(date, 2)),
monthly: (date: Date) => startOfMonth(addMonths(date, 2)),
quarterly: (date: Date) => startOfQuarter(addQuarters(date, 2)),
yearly: (date: Date) => startOfAssessLifeYear(addYears(date, 2), 12, 24)
}

const minDays = {
weekly: 3, // last three days
monthly: 21, // last three weeks
quarterly: 60, // last two months
yearly: 240 // last 8 months
}

let performAt: Date | undefined = undefined
let performIntervals: AssessLifeInterval[] = []

for (const interval of intervals) {
const start = startOfNextInterval[interval](now)
const next = getNextDay(start, settings.preferredDay)

// do not send reminder if the next reminder is too soon - and thus try to set next next reminder
const difference = differenceInDays(next, now)
if (difference > minDays[interval]) {

if (!performAt || isBefore(next, performAt)) {
performAt = next
performIntervals = [interval]
} else if (isEqual(next, performAt)) {
performIntervals.push(interval)
}

} else {
const startNextNext = startOfNextNextInterval[interval](now)
const nextNext = getNextDay(startNextNext, settings.preferredDay)

if (!performAt || isBefore(nextNext, performAt)) {
performAt = nextNext
performIntervals = [interval]
} else if (isEqual(nextNext, performAt)) {
performIntervals.push(interval)
}
}
}

if (!performAt) throw new Error('No performAt found')

const [hours, minutes] = settings.preferredTime.split(':').map(str => parseInt(str))
performAt.setHours(hours)
performAt.setMinutes(minutes)

return { performAt, performIntervals }
}

function getAvailableIntervals(settings: AssessLifeSettings) {
const availableIntervals = ['weekly', 'monthly', 'quarterly', 'yearly']
return unique(Object.values(settings).filter(interval => availableIntervals.includes(interval))) as AssessLifeInterval[]
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { WheelOfLifeSettings } from '@strive/model'
import { logger, onDocumentCreate, onDocumentDelete, onDocumentUpdate } from '@strive/api/firebase'
import { updateAggregation } from '../../../shared/scheduled-task/aggregation'
import { updateAggregation } from '../../../shared/aggregation/aggregation'
import { deleteScheduledTask, upsertScheduledTask } from '../../../shared/scheduled-task/scheduled-task'
import { enumWorkerType, ScheduledTaskUserExerciseWheelOfLife } from '../../../shared/scheduled-task/scheduled-task.interface'
import { addMonths, addQuarters, addWeeks, addYears } from 'date-fns'
Expand Down
1 change: 1 addition & 0 deletions apps/backend-functions/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export { scheduledFocusEmailRunner } from './pubsub/email/focus'
// firestorage
export { userSpectatorChangeHandler, userSpectatorCreatedHandler, userSpectatorDeleteHandler } from './firestore/users/user-spectators/user-spectator'
export { affirmationsCreatedHandler, affirmationsChangeHandler, affirmationsDeleteHandler } from './firestore/users/exercises/affirmation'
export { assessLifeSettingsCreatedHandler, assessLifeSettingsChangeHandler, assessLifeEntryCreatedHandler } from './firestore/users/exercises/assess_life'
export { dailyGratitudeCreatedHandler, dailyGratitudeChangedHandler, dailyGratitudeDeleteHandler } from './firestore/users/exercises/daily_gratitude'
export { dearFutureSelfCreatedHandler, dearFutureSelfChangedHandler, dearFutureSelfDeleteHandler } from './firestore/users/exercises/dear_future_self'
export { wheelOfLifeCreatedHandler, wheelOfLifeChangedHandler, wheelOfLifeDeleteHandler, wheelOfLifeEntryCreatedHandler } from './firestore/users/exercises/wheel_of_life'
Expand Down
16 changes: 13 additions & 3 deletions apps/backend-functions/src/pubsub/scheduled-task-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
ScheduledTaskGoalInviteLinkDeadline,
ScheduledTaskMilestoneDeadline,
ScheduledTaskUserExerciseAffirmations,
ScheduledTaskUserExerciseAssessLife,
ScheduledTaskUserExerciseDailyGratitude,
ScheduledTaskUserExerciseDearFutureSelfMessage,
ScheduledTaskUserExerciseWheelOfLife
Expand All @@ -20,8 +21,9 @@ import { sendAffirmationPushNotification, scheduleNextAffirmation } from './user
import { scheduleNextReminder, sendWheelOfLifePushNotification } from './user-exercises/wheel_of_life'
import { sendDearFutureSelfEmail, sendDearFutureSelfPushNotification } from './user-exercises/dear_future_self'

import { DearFutureSelf, Personal, Affirmations, WheelOfLifeSettings, createGoalSource } from '@strive/model'
import { DearFutureSelf, Personal, Affirmations, WheelOfLifeSettings, createGoalSource, AssessLifeSettings } from '@strive/model'
import { AES, enc } from 'crypto-js'
import { scheduleNextAssessLifeReminder, sendAssessLifePuthNotification } from './user-exercises/assess_life'

// https://fireship.io/lessons/cloud-functions-scheduled-time-trigger/
// crontab.guru to determine schedule value
Expand All @@ -42,7 +44,8 @@ async () => {
const reschedulingTasks = [
enumWorkerType.userExerciseAffirmation,
enumWorkerType.userExerciseDailyGratitudeReminder,
enumWorkerType.userExerciseWheelOfLifeReminder
enumWorkerType.userExerciseWheelOfLifeReminder,
enumWorkerType.userExerciseAssessLife
]

// Loop over documents and push job.
Expand Down Expand Up @@ -84,7 +87,8 @@ const workers: IWorkers = {
userExerciseAffirmation: (options) => userExerciseAffirmationsHandler(options),
userExerciseDailyGratitudeReminder: (options) => userExerciseDailyGratitudeReminderHandler(options),
userExerciseDearFutureSelfMessage: (options) => userExerciseDearFutureSelfMessageHandler(options),
userExerciseWheelOfLifeReminder: (options) => userExerciseWheelOfLifeReminderHandler(options)
userExerciseWheelOfLifeReminder: (options) => userExerciseWheelOfLifeReminderHandler(options),
userExerciseAssessLife: (options) => userExerciseAssessLifeHandler(options)
}

function deleteInviteLinkGoal(options: ScheduledTaskGoalInviteLinkDeadline['options']) {
Expand Down Expand Up @@ -143,4 +147,10 @@ async function userExerciseWheelOfLifeReminderHandler(options: ScheduledTaskUser

// reschedule task for next interval
scheduleNextReminder(settings, options.userId)
}

async function userExerciseAssessLifeHandler(options: ScheduledTaskUserExerciseAssessLife['options']) {
const settings = await getDocument<AssessLifeSettings>(`Users/${options.userId}/Exercises/AssessLife`)
sendAssessLifePuthNotification(options)
scheduleNextAssessLifeReminder(settings, options.userId)
}
47 changes: 47 additions & 0 deletions apps/backend-functions/src/pubsub/user-exercises/assess_life.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { AssessLifeSettings, Personal, getInterval } from '@strive/model'
import type { Message } from 'firebase-admin/messaging'
import { getDocument } from '../../shared/utils'
import { admin } from '@strive/api/firebase'
import { ScheduledTaskUserExerciseAssessLife, enumWorkerType } from '../../shared/scheduled-task/scheduled-task.interface'
import { smartJoin } from '@strive/utils/helpers'
import { getNextReminder } from '../../firestore/users/exercises/assess_life'
import { upsertScheduledTask } from '../../shared/scheduled-task/scheduled-task'

export async function sendAssessLifePuthNotification(options: ScheduledTaskUserExerciseAssessLife['options']) {

const { userId, intervals } = options

const converted = intervals.map(interval => getInterval(interval))
const readable = smartJoin(converted, ', ', ' and ')

const personal = await getDocument<Personal>(`Users/${userId}/Personal/${userId}`)
const link = `goals?t=assesslife`
const messages: Message[] = personal.fcmTokens.map(token => ({
token,
notification: {
title: `Time to Assess Life`,
body: `Take a moment to do the ${readable} assessment`
},
data: { link },
webpush: {
notification: {
icon: 'https://firebasestorage.googleapis.com/v0/b/strive-journal.appspot.com/o/FCMImages%2Ficon-72x72.png?alt=media&token=19250b44-1aef-4ea6-bbaf-d888150fe4a9',
},
fcmOptions: { link }
}
}))
if (!messages.length) return
return admin.messaging().sendEach(messages)
}

export async function scheduleNextAssessLifeReminder(settings: AssessLifeSettings, userId: string) {
const { performAt, performIntervals: intervals } = getNextReminder(settings)

const task: ScheduledTaskUserExerciseAssessLife = {
worker: enumWorkerType.userExerciseAssessLife,
performAt,
options: { userId, intervals },
status: 'scheduled'
}
return upsertScheduledTask(`${userId}assesslife`, task)
}
Loading

0 comments on commit f193e17

Please sign in to comment.