feat(jef-38): add granular digest frequency - #290
Conversation
WalkthroughThe change replaces the weekly digest flag with a persisted daily, weekly, or off frequency. It updates the GraphQL API, settings page, digest scheduling, resend windows, email content, database migration, and related tests. ChangesDigest frequency configuration
Sequence Diagram(s)sequenceDiagram
participant VercelCron
participant SendWeeklyDigestUseCase
participant DrizzleUserRepository
participant BrevoEmailService
participant weeklyDigestTemplate
VercelCron->>SendWeeklyDigestUseCase: run daily digest job
SendWeeklyDigestUseCase->>DrizzleUserRepository: read digest frequency
SendWeeklyDigestUseCase->>BrevoEmailService: send daily or weekly digest
BrevoEmailService->>weeklyDigestTemplate: build matching email HTML
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
apps/api/src/use-cases/user/UpdateNotificationPreferencesUseCase.ts (1)
20-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse the constant, and make the precedence over
weeklyDigestEnabledexplicit.Two points on this block:
- Line 27 compares against the literal
'off'while the file importsDIGEST_FREQUENCY. UseDIGEST_FREQUENCY.OFFfor consistency.- If a caller sends both
digestFrequencyandweeklyDigestEnabled, the frequency silently wins. The GraphQL mutation still exposes both fields, so a client can send{ digestFrequency: 'daily', weeklyDigestEnabled: false }and receiveweeklyDigestEnabled: true. Record the precedence rule in a comment, or reject the conflicting combination.♻️ Proposed refactor
+ // `digestFrequency` takes precedence: `weeklyDigestEnabled` is kept in sync for + // legacy clients that still read it. const updateData: Parameters<IUserRepository['update']>[1] = { weeklyDigestEnabled: - frequency === 'off' ? false : frequency ? true : input.weeklyDigestEnabled, + frequency === DIGEST_FREQUENCY.OFF ? false : frequency ? true : input.weeklyDigestEnabled, followUpRemindersEnabled: input.followUpRemindersEnabled, pushNotificationsEnabled: input.pushNotificationsEnabled, };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/api/src/use-cases/user/UpdateNotificationPreferencesUseCase.ts` around lines 20 - 31, Update the updateData construction in UpdateNotificationPreferencesUseCase to compare frequency with DIGEST_FREQUENCY.OFF instead of the literal 'off'. Explicitly document that a provided digestFrequency takes precedence over weeklyDigestEnabled, or reject conflicting inputs if that is the intended contract; preserve the existing behavior otherwise.apps/api/src/constants.ts (1)
396-399: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExport a derived union type for the frequency values.
The literal union
'daily' | 'weekly' | 'off'is repeated inapps/api/src/domain/user/User.ts,apps/api/src/use-cases/ports/IUserRepository.ts,apps/api/src/infrastructure/db/repositories/DrizzleUserRepository.tsandapps/api/src/use-cases/user/IUpdateNotificationPreferencesUseCase.ts. Derive the type once here, then import it. This keeps the constant and the type in step when a value is added.♻️ Proposed refactor
export const DIGEST_FREQUENCY = { DAILY: 'daily', WEEKLY: 'weekly', OFF: 'off', } as const; + +export type DigestFrequency = (typeof DIGEST_FREQUENCY)[keyof typeof DIGEST_FREQUENCY];🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/api/src/constants.ts` around lines 396 - 399, Derive and export a `DigestFrequency` union type from the `DIGEST_FREQUENCY` values in `constants.ts`, then replace the repeated literal unions in `User`, `IUserRepository`, `DrizzleUserRepository`, and `IUpdateNotificationPreferencesUseCase` with imports of that shared type. Keep the existing constant values and behavior unchanged.apps/api/src/infrastructure/db/schema.ts (1)
13-13: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueNarrow the column type to remove the mapper cast.
Apply
$typetodigestFrequencyinapps/api/src/infrastructure/db/schema.ts:13, thenDrizzleUserRepository.toEntitycan includedigestFrequency: row.digestFrequencywithoutas User['digestFrequency']. This only changes the compile-time type; the SQLite column remains a plain TEXT column.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/api/src/infrastructure/db/schema.ts` at line 13, Update the digestFrequency column definition to use Drizzle’s $type with the User digest-frequency type, then remove the corresponding User['digestFrequency'] cast in DrizzleUserRepository.toEntity while preserving the plain SQLite TEXT storage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/api/src/constants.ts`:
- Around line 393-394: Update the weekly digest resend-window constant near
DAILY_RESEND_AFTER to use approximately 7 days minus one hour: set it to 6 days
and 23 hours in milliseconds, preserving the existing constant naming and
readonly configuration.
In
`@apps/web/src/routes/_authenticated/settings/-components/SettingsNotificationsPage.tsx`:
- Around line 23-28: Align NotificationPreferences.digestFrequency with the
DigestFrequencyEnum across the API and web client: update
apps/api/src/http/schema/types/NotificationPreferencesType.ts:9 to expose the
enum, adjust the response/query type definitions in
apps/web/src/routes/_authenticated/settings/-components/shared.ts:156-166 and
:493-495 to match the GraphQL enum, and update digestFrequency state,
onChangeDigestFrequency, selector values, and mutation payload handling in
SettingsNotificationsPage.tsx:23-28 and :74-98 to use the schema’s enum values
consistently.
---
Nitpick comments:
In `@apps/api/src/constants.ts`:
- Around line 396-399: Derive and export a `DigestFrequency` union type from the
`DIGEST_FREQUENCY` values in `constants.ts`, then replace the repeated literal
unions in `User`, `IUserRepository`, `DrizzleUserRepository`, and
`IUpdateNotificationPreferencesUseCase` with imports of that shared type. Keep
the existing constant values and behavior unchanged.
In `@apps/api/src/infrastructure/db/schema.ts`:
- Line 13: Update the digestFrequency column definition to use Drizzle’s $type
with the User digest-frequency type, then remove the corresponding
User['digestFrequency'] cast in DrizzleUserRepository.toEntity while preserving
the plain SQLite TEXT storage.
In `@apps/api/src/use-cases/user/UpdateNotificationPreferencesUseCase.ts`:
- Around line 20-31: Update the updateData construction in
UpdateNotificationPreferencesUseCase to compare frequency with
DIGEST_FREQUENCY.OFF instead of the literal 'off'. Explicitly document that a
provided digestFrequency takes precedence over weeklyDigestEnabled, or reject
conflicting inputs if that is the intended contract; preserve the existing
behavior otherwise.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 90552709-0c8c-4fee-ae31-05e8426d8c37
⛔ Files ignored due to path filters (4)
apps/web/src/graphql/generated/fragment-masking.tsis excluded by!**/generated/**apps/web/src/graphql/generated/gql.tsis excluded by!**/generated/**apps/web/src/graphql/generated/graphql.tsis excluded by!**/generated/**apps/web/src/graphql/generated/index.tsis excluded by!**/generated/**
📒 Files selected for processing (31)
apps/api/drizzle/0015_strong_silver_surfer.sqlapps/api/drizzle/meta/0015_snapshot.jsonapps/api/drizzle/meta/_journal.jsonapps/api/src/__tests__/application/user/GetNotificationPreferencesUseCase.test.tsapps/api/src/__tests__/digest/SendWeeklyDigestUseCase.test.tsapps/api/src/__tests__/helpers/createTestDb.tsapps/api/src/__tests__/helpers/mocks.tsapps/api/src/__tests__/http/container.test.tsapps/api/src/__tests__/infrastructure/db/repositories/DrizzleDocumentRepository.test.tsapps/api/src/constants.tsapps/api/src/domain/user/User.tsapps/api/src/http/schema/index.tsapps/api/src/http/schema/mutations/userMutations.tsapps/api/src/http/schema/types/NotificationPreferencesType.tsapps/api/src/http/schema/types/enums/DigestFrequencyEnum.tsapps/api/src/infrastructure/db/repositories/DrizzleUserRepository.tsapps/api/src/infrastructure/db/schema.tsapps/api/src/infrastructure/email/BrevoEmailService.tsapps/api/src/infrastructure/email/templates/weeklyDigestTemplate.tsapps/api/src/interface-adapters/resolvers/UserResolver.tsapps/api/src/use-cases/digest/SendWeeklyDigestUseCase.tsapps/api/src/use-cases/ports/IEmailService.tsapps/api/src/use-cases/ports/IUserRepository.tsapps/api/src/use-cases/user/GetNotificationPreferencesUseCase.tsapps/api/src/use-cases/user/IGetNotificationPreferencesUseCase.tsapps/api/src/use-cases/user/IUpdateNotificationPreferencesUseCase.tsapps/api/src/use-cases/user/UpdateNotificationPreferencesUseCase.tsapps/api/vercel.jsonapps/web/src/__tests__/components/settings/SettingsNotificationsPage.test.tsxapps/web/src/routes/_authenticated/settings/-components/SettingsNotificationsPage.tsxapps/web/src/routes/_authenticated/settings/-components/shared.ts
| DAILY_RESEND_AFTER: 23 * 60 * 60 * 1000, // 23 hours | ||
| } as const; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Inspect the digest cron schedule and every use of the resend windows.
fd -t f 'vercel.json' --exec cat {}
rg -n -C4 'RESEND_AFTER' --type=tsRepository: mankatcheung/job-finder
Length of output: 364
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== constants.ts relevant lines =="
sed -n '360,405p' apps/api/src/constants.ts
echo
echo "== RESEND_AFTER usages =="
rg -n -C5 'RESEND_AFTER|DAILY_RESEND_AFTER' --type=ts --type=json
echo
echo "== digest send path files =="
rg -n -C3 'admin/digest/send|/digest/send|digest/send' --type=ts --type=json --type=graphql -- .Repository: mankatcheung/job-finder
Length of output: 2272
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== candidate git status/diff summary =="
git diff --stat || true
echo
echo "== source files mentioning digest resend window constants =="
rg -n -C4 'DIGEST_WINDOW_MS|RESEND_AFTER|DAILY_RESEND_AFTER|frequency|weekly|daily' apps/api/src --type=ts || true
echo
echo "== locate digest-related route/use-case files =="
git ls-files apps/api/src | rg -i 'digest|application|reminder|cron|schedule' || true
echo
echo "== search API route path =="
rg -n -C3 '"?/admin/digest/send"?|admin/digest/send|digest.*send|send.*digest' apps/api/src vercel.json --type=ts --type=json || trueRepository: mankatcheung/job-finder
Length of output: 50379
Align the weekly digest resend guard with the daily cron.
With RESEND_AFTER at 6 days and the digest cron running daily, weekly subscribers become eligible again 6 days after the previous send, so the effective cadence is 6 days rather than 7. Set the weekly resend window closer to 7 days minus a small tolerance, for example 6 * 24 + 23 hours.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/api/src/constants.ts` around lines 393 - 394, Update the weekly digest
resend-window constant near DAILY_RESEND_AFTER to use approximately 7 days minus
one hour: set it to 6 days and 23 hours in milliseconds, preserving the existing
constant naming and readonly configuration.
| const digestFrequency = prefs?.digestFrequency ?? (prefs?.weeklyDigestEnabled ? 'weekly' : 'off'); | ||
|
|
||
| const onToggleWeeklyDigest = async (checked: boolean) => { | ||
| await gqlClient.request(UPDATE_NOTIFICATION_PREFERENCES, { weeklyDigestEnabled: checked }); | ||
| const onChangeDigestFrequency = async ( | ||
| digestFrequency: NotificationPreferences['digestFrequency'], | ||
| ) => { | ||
| await gqlClient.request(UPDATE_NOTIFICATION_PREFERENCES, { digestFrequency }); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
rg -n \
"DigestFrequencyEnum|DigestFrequency|digestFrequency|exposeString|value=\"(daily|weekly|off|DAILY|WEEKLY|OFF)\"" \
apps/api/src/http/schema \
apps/web/src/routes/_authenticated/settings/-componentsRepository: mankatcheung/job-finder
Length of output: 18625
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
echo "== DigestFrequencyEnum =="
cat -n apps/api/src/http/schema/types/enums/DigestFrequencyEnum.ts
echo
echo "== NotificationPreferencesType =="
sed -n '1,20p' apps/api/src/http/schema/types/NotificationPreferencesType.ts | cat -n
echo
echo "== userMutations relevant =="
sed -n '335,375p' apps/api/src/http/schema/mutations/userMutations.ts | cat -n
echo
echo "== fetch/resolve usages of updateNotificationPreferences and notify prefs =="
rg -n "updateNotificationPreferences|NotificationPreferences|weeklyDigestEnabled|--normalize" apps/api/src apps/web/src/routes/_authenticated/settings -g '*.ts' -g '*.tsx'Repository: mankatcheung/job-finder
Length of output: 17744
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
echo "== constants DIGEST_FREQUENCY =="
cat -n apps/api/src/constants.ts | sed -n '1,80p'
echo
echo "== user mutation enum imports/usages broad =="
rg -n "DigestFrequency|DIGEST_FREQUENCY|DAILY|WEEKLY|OFF" apps/api/src -g '*.ts'
echo
echo "== Web shared query/mutation/types relevant =="
for f in apps/web/src/routes/_authenticated/settings/-components/shared.ts apps/web/src/routes/_authenticated/settings/-components/SettingsNotificationsPage.tsx; do
echo "--- $f"
wc -l "$f"
done
sed -n '130,170p' apps/web/src/routes/_authenticated/settings/-components/shared.ts | cat -n
sed -n '480,500p' apps/web/src/routes/_authenticated/settings/-components/shared.ts | cat -n
sed -n '1,110p' apps/web/src/routes/_authenticated/settings/-components/SettingsNotificationsPage.tsx | cat -nRepository: mankatcheung/job-finder
Length of output: 13116
Align NotificationPreferences with the DigestFrequency GraphQL enum.
DigestFrequency maps enum values to daily, weekly, and off, and the mutation currently accepts those values. The response field is noticePreferences.digestFrequency, so the web type stays lower-case, but the GraphQL schema and client queries should treat it consistently.
apps/api/src/http/schema/types/NotificationPreferencesType.ts#L9: ExposedigestFrequencywithDigestFrequencyEnuminstead oft.exposeString, unless the GraphQL response intentionally uses raw API values.apps/web/src/routes/_authenticated/settings/-components/shared.ts#L493-L495: Keep the client response type aligned with the scalar/enum returned fromnotificationPreferences.apps/web/src/routes/_authenticated/settings/-components/SettingsNotificationsPage.tsx#L23-L28andL74-L98: Treat selector state and the mutation payload as GraphQL enum values if the schema uses upcased enum names.
📍 Affects 3 files
apps/web/src/routes/_authenticated/settings/-components/SettingsNotificationsPage.tsx#L23-L28(this comment)apps/web/src/routes/_authenticated/settings/-components/SettingsNotificationsPage.tsx#L74-L98apps/web/src/routes/_authenticated/settings/-components/shared.ts#L156-L166apps/web/src/routes/_authenticated/settings/-components/shared.ts#L493-L495apps/api/src/http/schema/types/NotificationPreferencesType.ts#L9-L9
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@apps/web/src/routes/_authenticated/settings/-components/SettingsNotificationsPage.tsx`
around lines 23 - 28, Align NotificationPreferences.digestFrequency with the
DigestFrequencyEnum across the API and web client: update
apps/api/src/http/schema/types/NotificationPreferencesType.ts:9 to expose the
enum, adjust the response/query type definitions in
apps/web/src/routes/_authenticated/settings/-components/shared.ts:156-166 and
:493-495 to match the GraphQL enum, and update digestFrequency state,
onChangeDigestFrequency, selector values, and mutation payload handling in
SettingsNotificationsPage.tsx:23-28 and :74-98 to use the schema’s enum values
consistently.
Summary
Add daily, weekly, and off choices for job-search digest emails.
Changes
Verification
Summary by CodeRabbit
New Features
Bug Fixes
Data Updates