Skip to content

feat(user): add notification preferences - #60

Merged
mankatcheung merged 2 commits into
mainfrom
feature/jef-21-notification-preferences
Jul 23, 2026
Merged

mankatcheung merged 2 commits into
mainfrom
feature/jef-21-notification-preferences

Conversation

@mankatcheung

@mankatcheung mankatcheung commented Jul 23, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Lets each user turn off the weekly digest and/or follow-up reminder emails independently — previously both were sent unconditionally to every user via SendWeeklyDigestUseCase/SendFollowUpRemindersUseCase
  • Scope note: intentionally narrower than the original ticket's "daily/weekly/off cadence + channel selection" — a daily digest pipeline doesn't exist (only weekly), and the Slack/Discord channel from JEF-15 hasn't landed. This PR wires preferences into the two notification paths that actually exist today; cadence/channel options are natural follow-ups once that infra exists.
  • New weeklyDigestEnabled/followUpRemindersEnabled columns on User, defaulting to true so existing users are unaffected
  • SendWeeklyDigestUseCase now skips users with the digest disabled (without even querying their applications); SendFollowUpRemindersUseCase skips applications for users with reminders disabled
  • New notificationPreferences query and updateNotificationPreferences mutation, backed by GetNotificationPreferencesUseCase/UpdateNotificationPreferencesUseCase
  • New Notification preferences section in account.tsx with two toggle checkboxes

Fixes JEF-21

Test plan

  • pnpm typecheck (api + web)
  • pnpm lint (api + web)
  • pnpm test — 410 API tests + 77 web tests passing, including new coverage for both use cases, the updated digest/reminder skip behavior, PrismaUserRepository, UserResolver, and the new toggles UI. (One pre-existing, unrelated flaky test in PrismaDocumentRepository — a timestamp-ordering race — reproduces on this branch too; confirmed it also fails standalone on 3 reruns with no changes of mine involved.)
  • pnpm build (all packages)
  • Manually verified end-to-end against a live server with an isolated throwaway DB: registered a user, confirmed both preferences default to true, called updateNotificationPreferences(weeklyDigestEnabled: false), confirmed it persisted while followUpRemindersEnabled stayed untouched

🤖 Generated with Claude Code

https://claude.ai/code/session_01DdEiNRTcUnM6kE5AdFQ3n8

Summary by CodeRabbit

  • New Features
    • Added account controls for enabling or disabling weekly digest emails.
    • Added account controls for enabling or disabling follow-up reminder emails.
    • Added notification preference retrieval and update support.
  • Bug Fixes
    • Disabled weekly digests and follow-up reminders are no longer sent when their settings are turned off.
  • Tests
    • Added coverage for preference loading, updating, UI toggles, and notification suppression.

Lets each user turn off the weekly digest and/or follow-up reminder
emails independently, instead of the previous fixed behavior baked into
SendWeeklyDigestUseCase and SendFollowUpRemindersUseCase.

Scoped to the two notification channels that actually exist today
(weekly digest email, follow-up reminder email) rather than the full
daily/weekly/off cadence and multi-channel design in the original ticket,
since a daily digest pipeline and Slack/Discord channel (JEF-15) don't
exist yet — those are natural follow-ups once that infrastructure lands.

- New weeklyDigestEnabled/followUpRemindersEnabled columns on User
  (default true, so existing users keep receiving both emails)
- SendWeeklyDigestUseCase skips users with weeklyDigestEnabled=false
  (counted as skipped, without querying their applications)
- SendFollowUpRemindersUseCase skips applications belonging to users
  with followUpRemindersEnabled=false
- New notificationPreferences query and updateNotificationPreferences
  mutation, backed by Get/UpdateNotificationPreferencesUseCase
- New Notification preferences section in account.tsx with two toggles
- Verified end-to-end against a live server: registered a user, confirmed
  both preferences default to enabled, disabled the weekly digest, and
  confirmed the change persisted while the reminder preference was
  untouched
@coderabbitai

coderabbitai Bot commented Jul 23, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@mankatcheung, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 36 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 05fadb69-0db1-4c03-a64c-ec8fa3f62d04

📥 Commits

Reviewing files that changed from the base of the PR and between a8697bf and 849b74c.

📒 Files selected for processing (17)
  • apps/api/prisma/schema.prisma
  • apps/api/src/__tests__/application/reminders/SendFollowUpRemindersUseCase.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__/infrastructure/db/repositories/PrismaUserRepository.test.ts
  • apps/api/src/__tests__/interface-adapters/resolvers/UserResolver.test.ts
  • apps/api/src/domain/user/User.ts
  • apps/api/src/http/container.ts
  • apps/api/src/http/schema/index.ts
  • apps/api/src/http/schema/mutations/userMutations.ts
  • apps/api/src/http/schema/queries/userQueries.ts
  • apps/api/src/infrastructure/db/repositories/PrismaUserRepository.ts
  • apps/api/src/interface-adapters/resolvers/UserResolver.ts
  • apps/api/src/use-cases/ports/IUserRepository.ts
  • apps/web/src/__tests__/components/AccountPage.test.tsx
  • apps/web/src/routes/_authenticated/account.tsx

Walkthrough

Adds persistent notification preferences for weekly digests and follow-up reminders, exposes them through GraphQL, provides account-page toggles, and prevents disabled notifications from being sent.

Changes

Notification preferences

Layer / File(s) Summary
Preference persistence and user contracts
apps/api/prisma/..., apps/api/src/domain/user/User.ts, apps/api/src/use-cases/ports/IUserRepository.ts, apps/api/src/infrastructure/db/repositories/..., apps/api/src/__tests__/helpers/..., apps/api/src/__tests__/infrastructure/...
User records, repository updates, Prisma mappings, test schemas, fixtures, and persistence tests include both preference fields with enabled defaults.
Preference use cases and notification enforcement
apps/api/src/use-cases/user/..., apps/api/src/use-cases/digest/..., apps/api/src/use-cases/reminders/..., apps/api/src/__tests__/application/...
New use cases retrieve and update preferences, while digest and reminder processing skip users who disable the relevant notification.
GraphQL API integration
apps/api/src/interface-adapters/resolvers/UserResolver.ts, apps/api/src/http/container.ts, apps/api/src/http/schema/..., apps/api/src/__tests__/interface-adapters/...
Dependency injection, resolver methods, GraphQL types, authentication guards, query and mutation fields expose preference retrieval and updates.
Account page controls and UI validation
apps/web/src/routes/_authenticated/account.tsx, apps/web/src/__tests__/components/AccountPage.test.tsx
The account page loads preferences, renders two checkboxes, submits changes, invalidates the query, and tests the resulting interactions.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  actor AccountUser
  participant AccountPage
  participant GraphQLAPI
  participant UserResolver
  participant UserRepository

  AccountUser->>AccountPage: Open account page
  AccountPage->>GraphQLAPI: Query notificationPreferences
  GraphQLAPI->>UserResolver: getNotificationPreferences(userId)
  UserResolver->>UserRepository: findById(userId)
  UserRepository-->>UserResolver: User preferences
  UserResolver-->>GraphQLAPI: NotificationPreferences
  GraphQLAPI-->>AccountPage: weeklyDigestEnabled and followUpRemindersEnabled
  AccountUser->>AccountPage: Toggle preference
  AccountPage->>GraphQLAPI: updateNotificationPreferences(values)
  GraphQLAPI->>UserResolver: updateNotificationPreferences(userId, values)
  UserResolver->>UserRepository: update(userId, values)
  UserRepository-->>UserResolver: Updated User
  GraphQLAPI-->>AccountPage: true
  AccountPage->>GraphQLAPI: Refetch notificationPreferences
Loading

Possibly related PRs

Poem

I’m a rabbit with toggles to spare,
Weekly carrots and reminders in care.
The schema hops wide,
While jobs step aside,
When preferences say, “Not today, hare!”

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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 matches the main change: adding user notification preferences.
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
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/jef-21-notification-preferences

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

🤖 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/infrastructure/db/repositories/PrismaUserRepository.ts`:
- Around line 58-68: Move the Prisma-row-to-domain conversion out of
PrismaUserRepository’s private toEntity method into a dedicated mapper under
interface-adapters/mappers/, then update PrismaUserRepository to call that
mapper and remove its local mapping implementation. Preserve the existing User
field mappings while keeping Prisma-specific conversion centralized in the
mapper layer.

In `@apps/web/src/routes/_authenticated/account.tsx`:
- Around line 112-121: Update onToggleWeeklyDigest and onToggleFollowUpReminders
to prevent overlapping preference mutations by disabling the relevant controls
or serializing updates while a request is pending, ensuring the final user
selection cannot be overwritten by an earlier response. Add failure handling for
the mutation and surface errors through the existing account UI feedback
mechanism.
🪄 Autofix (Beta)

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: 5f00b2ee-13be-4c4d-b7ff-2f1a01721386

📥 Commits

Reviewing files that changed from the base of the PR and between 46efcd4 and a8697bf.

📒 Files selected for processing (27)
  • apps/api/prisma/migrations/20260722231319_add_notification_preferences/migration.sql
  • apps/api/prisma/schema.prisma
  • apps/api/src/__tests__/application/reminders/SendFollowUpRemindersUseCase.test.ts
  • apps/api/src/__tests__/application/user/GetNotificationPreferencesUseCase.test.ts
  • apps/api/src/__tests__/application/user/UpdateNotificationPreferencesUseCase.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__/infrastructure/db/repositories/PrismaUserRepository.test.ts
  • apps/api/src/__tests__/interface-adapters/resolvers/UserResolver.test.ts
  • apps/api/src/domain/user/User.ts
  • apps/api/src/http/container.ts
  • apps/api/src/http/schema/index.ts
  • apps/api/src/http/schema/mutations/userMutations.ts
  • apps/api/src/http/schema/queries/userQueries.ts
  • apps/api/src/http/schema/types/NotificationPreferencesType.ts
  • apps/api/src/infrastructure/db/repositories/PrismaUserRepository.ts
  • apps/api/src/interface-adapters/resolvers/UserResolver.ts
  • apps/api/src/use-cases/digest/SendWeeklyDigestUseCase.ts
  • apps/api/src/use-cases/ports/IUserRepository.ts
  • apps/api/src/use-cases/reminders/SendFollowUpRemindersUseCase.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/web/src/__tests__/components/AccountPage.test.tsx
  • apps/web/src/routes/_authenticated/account.tsx

Comment on lines +58 to +68
weeklyDigestEnabled: boolean;
followUpRemindersEnabled: boolean;
createdAt: Date;
updatedAt: Date;
}): User {
return {
id: row.id,
email: row.email,
passwordHash: row.passwordHash,
weeklyDigestEnabled: row.weeklyDigestEnabled,
followUpRemindersEnabled: row.followUpRemindersEnabled,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Move Prisma-to-domain mapping into the prescribed mapper layer.

PrismaUserRepository now extends its private toEntity method to convert Prisma rows directly into the domain User. Put this conversion in interface-adapters/mappers/ and have the repository use that mapper, keeping persistence mapping centralised and aligned with the project architecture.

As per coding guidelines: apps/api/**/*.ts files must use mappers in interface-adapters/mappers/ to convert Prisma models to domain entities rather than coupling domain entities to Prisma.

🤖 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/repositories/PrismaUserRepository.ts` around
lines 58 - 68, Move the Prisma-row-to-domain conversion out of
PrismaUserRepository’s private toEntity method into a dedicated mapper under
interface-adapters/mappers/, then update PrismaUserRepository to call that
mapper and remove its local mapping implementation. Preserve the existing User
field mappings while keeping Prisma-specific conversion centralized in the
mapper layer.

Source: Coding guidelines

Comment on lines +112 to +121
const onToggleWeeklyDigest = async (checked: boolean) => {
await gqlClient.request(UPDATE_NOTIFICATION_PREFERENCES, { weeklyDigestEnabled: checked });
await qc.invalidateQueries({ queryKey: ['notificationPreferences'] });
};

const onToggleFollowUpReminders = async (checked: boolean) => {
await gqlClient.request(UPDATE_NOTIFICATION_PREFERENCES, {
followUpRemindersEnabled: checked,
});
await qc.invalidateQueries({ queryKey: ['notificationPreferences'] });

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 | 🟡 Minor | ⚡ Quick win

Prevent out-of-order preference writes.

The controls stay active during requests. Rapidly toggling one setting can leave overlapping mutations resolving out of order, persisting an earlier value instead of the user’s final choice. Disable or serialise updates while a preference mutation is pending, and show failures.

Also applies to: 327-340

🤖 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/account.tsx` around lines 112 - 121,
Update onToggleWeeklyDigest and onToggleFollowUpReminders to prevent overlapping
preference mutations by disabling the relevant controls or serializing updates
while a request is pending, ensuring the final user selection cannot be
overwritten by an earlier response. Add failure handling for the mutation and
surface errors through the existing account UI feedback mechanism.

@mankatcheung mankatcheung left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Overview

This PR adds per-user notification preferences (weeklyDigestEnabled / followUpRemindersEnabled), correctly wiring them through all Clean Architecture layers: Prisma schema/migration → domain User → IUserRepository.update → new Get/UpdateNotificationPreferencesUseCase → UserResolver → Pothos query/mutation → account.tsx toggles. Both existing sending pipelines (SendWeeklyDigestUseCase, SendFollowUpRemindersUseCase) now respect the flags. The PR description is transparent about scope being narrower than the original ticket (no daily cadence, no channel selection) since that infra doesn't exist yet — reasonable call.

Code Quality

  • Follows established conventions precisely: mutation/query auth checks (if (!ctx.user) throw GraphQLError(...)), ctx.user.sub scoping, fromCodedError wrapping, Awilix TRANSIENT registration for use cases — all match the sibling updateEmail/updatePassword/exportUserData code exactly.
  • SendWeeklyDigestUseCase short-circuits before querying applications for opted-out users — good, avoids unnecessary DB work as called out in the PR description.
  • Good test coverage added across layers: use-case unit tests (including the "leaves fields untouched when undefined" case, which correctly asserts Prisma-style partial update semantics), PrismaUserRepository integration tests via createTestDb, resolver tests, and web component tests for both toggles.
  • Migration is a straightforward additive ALTER TABLE ... DEFAULT true, matching the timestamp-prefixed naming convention of prior migrations and defaulting existing users to enabled (no behavior change on rollout).

Issues & Risks

  • Silent failure on toggle mutations (apps/web/.../account.tsx, onToggleWeeklyDigest/onToggleFollowUpReminders): unlike every other mutation in this file (email, password, delete — all use setError('root', ...) to surface failures), these two have no try/catch at all. A failed mutation becomes an unhandled promise rejection with zero user feedback — the checkbox will just silently not update. Even the "silent" export-download catch has an explicit comment justifying it; this one has neither a catch nor a comment.
  • getNotificationPreferences query resolver doesn't wrap the use-case call in try/catch + fromCodedError the way the mutation does — if GetNotificationPreferencesUseCase throws its NOT_FOUND coded error, it'll surface as a raw GraphQL error without the coded extensions. Low risk in practice (an authenticated user's own row won't normally be missing) but inconsistent with the mutation's handling one function away. (Note: exportUserData has this same gap pre-existing, so it's not a regression introduced here, but worth fixing while touching this file.)
  • Authorization is correctly self-scoped everywhere (ctx.user.sub only) — no way to read/update another user's preferences. No concerns there.
  • Migration safety: additive, backward-compatible, defaults preserve existing behavior. No concerns.

Suggestions

  • Wrap the two onToggleWeeklyDigest/onToggleFollowUpReminders handlers in try/catch and surface an error message (a small inline error banner under the toggles, consistent with the rest of the page) instead of letting failures pass silently.
  • Consider reverting the checkbox to its prior state on failure (currently relies on it never having visually flipped since it's driven by server state + invalidateQueries, so this is mostly a UX/feedback gap rather than a correctness bug).
  • Optional: align notificationPreferences query error handling with the mutation's fromCodedError wrapping for consistency (can be done together with the pre-existing exportUserData gap in a follow-up).

Test Coverage

Strong and appropriately layered: new use-case tests (happy path, not-found, partial-update semantics), repository integration tests via the real in-memory DB helper, resolver delegation tests, digest/reminder skip-behavior tests, and web component tests covering both toggle interactions and the loading of initial state. No coverage gap of concern; the one thing not covered is a toggle-mutation failure path (understandably, since there's no error handling to test yet).

Verdict

Approving — the implementation is correct, well-scoped, properly authorized, and thoroughly tested. The missing error handling on the toggle handlers is a real but minor UX gap, not a blocker; recommend addressing in a fast-follow.

@mankatcheung
mankatcheung merged commit 5b07183 into main Jul 23, 2026
5 checks passed
@mankatcheung
mankatcheung deleted the feature/jef-21-notification-preferences branch July 23, 2026 15:03
@coderabbitai coderabbitai Bot mentioned this pull request Jul 24, 2026
4 of 5 tasks
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