Skip to content

Commit e42d31d

Browse files
authored
Merge pull request #1737 from Mephistic/fix-next-digest-at
Tweak nextDigestAt Logic
2 parents 483b1c2 + 69fb65d commit e42d31d

File tree

5 files changed

+126
-21
lines changed

5 files changed

+126
-21
lines changed

.github/workflows/repo-checks.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ jobs:
1919
if: always()
2020
run: yarn check-formatting
2121

22-
# TODO: Add unit tests
23-
# - name: Unit Tests
24-
# if: always()
25-
# run: yarn test
22+
# TODO: Fix non-functions unit tests
23+
- name: Unit Tests
24+
if: always()
25+
run: yarn test
2626

2727
- name: Types
2828
if: always()
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { getNextDigestAt, getNotificationStartDate } 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+
})
83+
84+
describe("getNotificationStartDate", () => {
85+
it("should return the previous Tuesday for weekly frequency", () => {
86+
const fixedDate = Timestamp.fromDate(new Date(2025, 2, 11))
87+
const expectedDate = Timestamp.fromDate(new Date(2025, 2, 4))
88+
89+
const result = getNotificationStartDate("Weekly", fixedDate)
90+
expect(result).toEqual(expectedDate)
91+
})
92+
93+
it("should return the previous 1st of the month for monthly frequency", () => {
94+
const fixedDate = Timestamp.fromDate(new Date(2025, 2, 1))
95+
const expectedDate = Timestamp.fromDate(new Date(2025, 1, 1))
96+
97+
const result = getNotificationStartDate("Monthly", fixedDate)
98+
expect(result).toEqual(expectedDate)
99+
})
100+
})

functions/src/notifications/helpers.ts

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
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 {
4+
startOfDay,
5+
addMonths,
6+
setDate,
7+
previousTuesday,
8+
nextTuesday,
9+
subMonths
10+
} from "date-fns"
411

5-
// TODO - Unit tests
612
export const getNextDigestAt = (notificationFrequency: Frequency) => {
713
const now = startOfDay(new Date())
814
let nextDigestAt = null
915
switch (notificationFrequency) {
1016
case "Weekly":
11-
const weekAhead = new Date(now)
12-
weekAhead.setDate(weekAhead.getDate() + 7)
13-
nextDigestAt = Timestamp.fromDate(weekAhead)
17+
nextDigestAt = Timestamp.fromDate(nextTuesday(now))
1418
break
1519
case "Monthly":
16-
const monthAhead = new Date(now)
17-
monthAhead.setMonth(monthAhead.getMonth() + 1)
18-
nextDigestAt = Timestamp.fromDate(monthAhead)
20+
// Monthly notifications are sent on the 1st of the month
21+
const nextMonthFirst = setDate(addMonths(now, 1), 1)
22+
nextDigestAt = Timestamp.fromDate(nextMonthFirst)
1923
break
2024
case "None":
2125
nextDigestAt = null
@@ -34,13 +38,11 @@ export const getNotificationStartDate = (
3438
) => {
3539
switch (notificationFrequency) {
3640
case "Weekly":
37-
const weekAgo = new Date(now.toDate())
38-
weekAgo.setDate(weekAgo.getDate() - 7)
39-
return Timestamp.fromDate(weekAgo)
41+
return Timestamp.fromDate(previousTuesday(now.toDate()))
4042
case "Monthly":
41-
const monthAgo = new Date(now.toDate())
42-
monthAgo.setMonth(monthAgo.getMonth() - 1)
43-
return Timestamp.fromDate(monthAgo)
43+
const firstOfMonth = setDate(now.toDate(), 1)
44+
const previousFirstOfMonth = subMonths(firstOfMonth, 1)
45+
return Timestamp.fromDate(previousFirstOfMonth)
4446
// We can safely fallthrough here because if the user has no notification frequency set,
4547
// we won't even send a notification
4648
default:

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"lint:fix": "next lint --fix",
3535
"serve": "firebase emulators:start --only hosting",
3636
"start": "next start",
37+
"test": "yarn --cwd functions test",
3738
"test:e2e": "playwright test --ui",
3839
"test:e2e:headless": "playwright test",
3940
"test:integration": "jest -c tests/jest.integration.config.ts -w 1",

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)