Skip to content

feat(jef-38): add granular digest frequency - #290

Merged
mankatcheung merged 4 commits into
mainfrom
feature/jef-38-granular-weekly-digest-frequency
Aug 8, 2026
Merged

mankatcheung merged 4 commits into
mainfrom
feature/jef-38-granular-weekly-digest-frequency

Conversation

@mankatcheung

@mankatcheung mankatcheung commented Aug 8, 2026 •

Copy link
Copy Markdown
Owner

Summary

Add daily, weekly, and off choices for job-search digest emails.

Changes

  • Persist digestFrequency with weekly default and migrate existing disabled users to off
  • Keep the legacy weeklyDigestEnabled field synchronized
  • Add DigestFrequency GraphQL enum and notification preference API support
  • Run the digest cron daily and enforce per-user daily or weekly resend windows
  • Use 24-hour application/follow-up windows for daily digests and 7-day windows for weekly digests
  • Update digest email subjects and headings for daily versus weekly delivery
  • Replace the notification checkbox with a Daily / Weekly / Off selector
  • Regenerate web GraphQL artifacts

Verification

  • API: 173 test files, 1021 tests passed
  • Web: 29 test files, 243 tests passed
  • API and web typechecks passed
  • API and web production builds passed

Summary by CodeRabbit

  • New Features

    • Added digest frequency preferences: daily, weekly, or off.
    • Users can select their preferred frequency from notification settings.
    • Digest emails now reflect the selected delivery frequency.
    • Daily digest processing and delivery are supported.
  • Bug Fixes

    • Disabled digest preferences are now respected when sending notifications.
    • Digest timing and resend windows now align with the selected frequency.
  • Data Updates

    • Existing notification preferences are migrated with weekly delivery enabled by default.

@coderabbitai

coderabbitai Bot commented Aug 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The 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.

Changes

Digest frequency configuration

Layer / File(s) Summary
Persisted digest frequency contracts
apps/api/drizzle/..., apps/api/src/domain/user/User.ts, apps/api/src/infrastructure/db/..., apps/api/src/use-cases/..., apps/api/src/__tests__/...
The user schema stores digestFrequency with a weekly default. Existing disabled weekly digests are backfilled to off. Domain, repository, and preference contracts expose the daily, weekly, and off values.
GraphQL and settings preference flow
apps/api/src/http/schema/..., apps/api/src/interface-adapters/resolvers/UserResolver.ts, apps/api/src/use-cases/user/UpdateNotificationPreferencesUseCase.ts, apps/web/src/routes/..., apps/web/src/__tests__/...
The API accepts and returns digestFrequency. The settings page replaces the weekly checkbox with a frequency selector and persists the selected value.
Daily and weekly digest delivery
apps/api/src/use-cases/digest/SendWeeklyDigestUseCase.ts, apps/api/src/infrastructure/email/..., apps/api/src/use-cases/ports/IEmailService.ts, apps/api/vercel.json, apps/api/src/__tests__/digest/...
The daily cron job selects frequency-specific eligibility, resend windows, date periods, subjects, and email content. Off users are skipped.
Supporting test determinism
apps/api/src/__tests__/http/container.test.ts, apps/api/src/__tests__/infrastructure/db/repositories/DrizzleDocumentRepository.test.ts
The container test uses a 15-second timeout. Document ordering tests assign fixed timestamps.
Estimated code review effort: 4 (Complex) ~45 minutes

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
Loading

Possibly related PRs

Poem

A rabbit selects “daily” with care,
“Weekly” hops through scheduled air.
“Off” stays still, no mail is sent,
The digest finds its new intent.
Squeak—clean settings, neatly penned!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding granular daily, weekly, or off digest frequency options.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/jef-38-granular-weekly-digest-frequency

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
apps/api/src/use-cases/user/UpdateNotificationPreferencesUseCase.ts (1)

20-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the constant, and make the precedence over weeklyDigestEnabled explicit.

Two points on this block:

  1. Line 27 compares against the literal 'off' while the file imports DIGEST_FREQUENCY. Use DIGEST_FREQUENCY.OFF for consistency.
  2. If a caller sends both digestFrequency and weeklyDigestEnabled, the frequency silently wins. The GraphQL mutation still exposes both fields, so a client can send { digestFrequency: 'daily', weeklyDigestEnabled: false } and receive weeklyDigestEnabled: 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 win

Export a derived union type for the frequency values.

The literal union 'daily' | 'weekly' | 'off' is repeated in apps/api/src/domain/user/User.ts, apps/api/src/use-cases/ports/IUserRepository.ts, apps/api/src/infrastructure/db/repositories/DrizzleUserRepository.ts and apps/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 value

Narrow the column type to remove the mapper cast.

Apply $type to digestFrequency in apps/api/src/infrastructure/db/schema.ts:13, then DrizzleUserRepository.toEntity can include digestFrequency: row.digestFrequency without as 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

📥 Commits

Reviewing files that changed from the base of the PR and between fc10538 and 2c741be.

⛔ Files ignored due to path filters (4)
  • apps/web/src/graphql/generated/fragment-masking.ts is excluded by !**/generated/**
  • apps/web/src/graphql/generated/gql.ts is excluded by !**/generated/**
  • apps/web/src/graphql/generated/graphql.ts is excluded by !**/generated/**
  • apps/web/src/graphql/generated/index.ts is excluded by !**/generated/**
📒 Files selected for processing (31)
  • apps/api/drizzle/0015_strong_silver_surfer.sql
  • apps/api/drizzle/meta/0015_snapshot.json
  • apps/api/drizzle/meta/_journal.json
  • apps/api/src/__tests__/application/user/GetNotificationPreferencesUseCase.test.ts
  • apps/api/src/__tests__/digest/SendWeeklyDigestUseCase.test.ts
  • apps/api/src/__tests__/helpers/createTestDb.ts
  • apps/api/src/__tests__/helpers/mocks.ts
  • apps/api/src/__tests__/http/container.test.ts
  • apps/api/src/__tests__/infrastructure/db/repositories/DrizzleDocumentRepository.test.ts
  • apps/api/src/constants.ts
  • apps/api/src/domain/user/User.ts
  • apps/api/src/http/schema/index.ts
  • apps/api/src/http/schema/mutations/userMutations.ts
  • apps/api/src/http/schema/types/NotificationPreferencesType.ts
  • apps/api/src/http/schema/types/enums/DigestFrequencyEnum.ts
  • apps/api/src/infrastructure/db/repositories/DrizzleUserRepository.ts
  • apps/api/src/infrastructure/db/schema.ts
  • apps/api/src/infrastructure/email/BrevoEmailService.ts
  • apps/api/src/infrastructure/email/templates/weeklyDigestTemplate.ts
  • apps/api/src/interface-adapters/resolvers/UserResolver.ts
  • apps/api/src/use-cases/digest/SendWeeklyDigestUseCase.ts
  • apps/api/src/use-cases/ports/IEmailService.ts
  • apps/api/src/use-cases/ports/IUserRepository.ts
  • apps/api/src/use-cases/user/GetNotificationPreferencesUseCase.ts
  • apps/api/src/use-cases/user/IGetNotificationPreferencesUseCase.ts
  • apps/api/src/use-cases/user/IUpdateNotificationPreferencesUseCase.ts
  • apps/api/src/use-cases/user/UpdateNotificationPreferencesUseCase.ts
  • apps/api/vercel.json
  • apps/web/src/__tests__/components/settings/SettingsNotificationsPage.test.tsx
  • apps/web/src/routes/_authenticated/settings/-components/SettingsNotificationsPage.tsx
  • apps/web/src/routes/_authenticated/settings/-components/shared.ts

Comment thread apps/api/src/constants.ts
Comment on lines +393 to +394
DAILY_RESEND_AFTER: 23 * 60 * 60 * 1000, // 23 hours
} as const;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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=ts

Repository: 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 || true

Repository: 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.

Comment on lines +23 to +28
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 });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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/-components

Repository: 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 -n

Repository: 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: Expose digestFrequency with DigestFrequencyEnum instead of t.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 from notificationPreferences.
  • apps/web/src/routes/_authenticated/settings/-components/SettingsNotificationsPage.tsx#L23-L28 and L74-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-L98
  • apps/web/src/routes/_authenticated/settings/-components/shared.ts#L156-L166
  • apps/web/src/routes/_authenticated/settings/-components/shared.ts#L493-L495
  • apps/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.

@mankatcheung
mankatcheung merged commit 86a835d into main Aug 8, 2026
11 checks passed
@mankatcheung
mankatcheung deleted the feature/jef-38-granular-weekly-digest-frequency branch August 18, 2026 13:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant