Skip to content

Commit 630225a

Browse files
committed
fix: changing nextDigestAt logic to lock to the specific sending days - not just to key off the current time plus one week/month. This should make this more resilient to bugs caused by inconsistent data. Also adding unit tests to cover this function now that it is getting more complicated
1 parent 80c7c75 commit 630225a

File tree

3 files changed

+99
-11
lines changed

3 files changed

+99
-11
lines changed
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { getNextDigestAt } from "./helpers"
2+
import { Timestamp } from "../firebase"
3+
import { Frequency } from "../auth/types"
4+
5+
describe("getNextDigestAt", () => {
6+
beforeEach(() => {
7+
jest.useFakeTimers()
8+
})
9+
10+
afterEach(() => {
11+
jest.useRealTimers()
12+
})
13+
14+
it("should return the next Tuesday for weekly frequency", () => {
15+
// Set a fixed date: Monday, March 3, 2025
16+
const fixedDate = new Date(2025, 2, 3)
17+
18+
// Next Tuesday, March 4, 2025
19+
const expectedDate = new Date(2025, 2, 4)
20+
21+
jest.setSystemTime(fixedDate)
22+
23+
const result = getNextDigestAt("Weekly")
24+
expect(result).toEqual(Timestamp.fromDate(expectedDate))
25+
})
26+
27+
// Extra test covering this case because getting this
28+
// wrong could cause an expensive infinite function loop
29+
it("should return the next Tuesday for weekly frequency when the current day is Tuesday", () => {
30+
// Set a fixed date: Tuesday, March 4, 2025
31+
const fixedDate = new Date(2025, 2, 4)
32+
33+
// Next Tuesday, March 11, 2025
34+
const expectedDate = new Date(2025, 2, 11)
35+
36+
jest.setSystemTime(fixedDate)
37+
38+
const result = getNextDigestAt("Weekly")
39+
expect(result).toEqual(Timestamp.fromDate(expectedDate))
40+
})
41+
42+
it("should return the next 1st of the month for monthly frequency", () => {
43+
// Set a fixed date: March 15, 2025
44+
const fixedDate = new Date(2025, 2, 15)
45+
// Next 1st of the month, April 1, 2025
46+
const expectedDate = new Date(2025, 3, 1)
47+
48+
jest.setSystemTime(fixedDate)
49+
50+
const result = getNextDigestAt("Monthly")
51+
expect(result).toEqual(Timestamp.fromDate(expectedDate))
52+
})
53+
54+
// Extra test covering this case because getting this
55+
// wrong could cause an expensive infinite function loop
56+
it("should return the next 1st of the month for monthly frequency when it is the 1st of the month", () => {
57+
// Set a fixed date: March 1, 2025
58+
const fixedDate = new Date(2025, 2, 1)
59+
// Next 1st of the month, April 1, 2025
60+
const expectedDate = new Date(2025, 3, 1)
61+
62+
jest.setSystemTime(fixedDate)
63+
64+
const result = getNextDigestAt("Monthly")
65+
expect(result).toEqual(Timestamp.fromDate(expectedDate))
66+
})
67+
68+
it("should return null for none frequency", () => {
69+
const result = getNextDigestAt("None")
70+
expect(result).toBeNull()
71+
})
72+
73+
it("should log an error and return null for unknown frequency", () => {
74+
const consoleSpy = jest.spyOn(console, "error").mockImplementation()
75+
const result = getNextDigestAt("SomeNewFrequency" as Frequency)
76+
expect(result).toBeNull()
77+
expect(consoleSpy).toHaveBeenCalledWith(
78+
"Unknown notification frequency: SomeNewFrequency"
79+
)
80+
consoleSpy.mockRestore()
81+
})
82+
})

functions/src/notifications/helpers.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
1-
import { Timestamp } from "firebase-admin/firestore"
1+
import { Timestamp } from "../firebase"
22
import { Frequency } from "../auth/types"
3-
import { startOfDay } from "date-fns"
3+
import { startOfDay, addDays, addMonths, setDay, setDate } from "date-fns"
44

5-
// TODO - Unit tests
65
export const getNextDigestAt = (notificationFrequency: Frequency) => {
76
const now = startOfDay(new Date())
87
let nextDigestAt = null
98
switch (notificationFrequency) {
109
case "Weekly":
11-
const weekAhead = new Date(now)
12-
weekAhead.setDate(weekAhead.getDate() + 7)
13-
nextDigestAt = Timestamp.fromDate(weekAhead)
10+
// Weekly notifications are sent on Tuesdays
11+
// - this should be the next available Tuesday
12+
const nextTuesday = setDay(now, 2, { weekStartsOn: 1 })
13+
nextDigestAt = Timestamp.fromDate(
14+
nextTuesday <= now ? addDays(nextTuesday, 7) : nextTuesday
15+
)
1416
break
1517
case "Monthly":
16-
const monthAhead = new Date(now)
17-
monthAhead.setMonth(monthAhead.getMonth() + 1)
18-
nextDigestAt = Timestamp.fromDate(monthAhead)
18+
// Monthly notifications are sent on the 1st of the month
19+
const nextMonthFirst = setDate(addMonths(now, 1), 1)
20+
nextDigestAt = Timestamp.fromDate(nextMonthFirst)
1921
break
2022
case "None":
2123
nextDigestAt = null
@@ -28,6 +30,8 @@ export const getNextDigestAt = (notificationFrequency: Frequency) => {
2830
return nextDigestAt
2931
}
3032

33+
// TODO: Rewrite with date-fns for consistency
34+
// TODO: Unit tests
3135
export const getNotificationStartDate = (
3236
notificationFrequency: Frequency,
3337
now: Timestamp

scripts/firebase-admin/backfillNextDigestAt.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ const Args = Record({
1717
dryRun: Boolean
1818
})
1919

20-
export const script: Script = async ({ db, auth, args }) => {
20+
export const script: Script = async ({ db, args }) => {
2121
const profilesSnapshot = await db.collection("profiles").get()
2222

23+
console.log(profilesSnapshot.docs.length, "profiles found")
24+
2325
const updatePromises = profilesSnapshot.docs.map(async profileDoc => {
2426
const profile = profileDoc.data() as Profile
2527

@@ -30,7 +32,7 @@ export const script: Script = async ({ db, auth, args }) => {
3032
await profileDoc.ref.update({ nextDigestAt })
3133
}
3234
console.log(
33-
`Updated nextDigestAt for ${profileDoc.id} to ${nextDigestAt}`
35+
`Updated nextDigestAt for ${profileDoc.id} to ${nextDigestAt?.toDate()}`
3436
)
3537
} else {
3638
console.log(

0 commit comments

Comments
 (0)