+
Skip to content

Conversation

steven-tey
Copy link
Collaborator

@steven-tey steven-tey commented Sep 24, 2025

Summary by CodeRabbit

  • New Features

    • Sends notification emails for bounty program updates to relevant partners and users.
  • Improvements

    • Uses per-user recipients for partner and program notifications for more accurate delivery.
    • More reliable detection of the latest unread messages to avoid missed or out-of-order notifications.
    • Email open events now mark related messages as read automatically.
  • Bug Fixes

    • Prevents re-marking messages as read by updating only messages not yet read in-app.
  • Chores

    • Standardized identifiers for notification emails to improve tracking.

Copy link
Contributor

vercel bot commented Sep 24, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Updated (UTC)
dub Ready Ready Preview Sep 25, 2025 0:42am

Copy link
Contributor

coderabbitai bot commented Sep 24, 2025

Walkthrough

Adds per-recipient NotificationEmail tracking and related schema fields, introduces an "em_" ID prefix, updates cron handlers to send batch emails per-user and persist notificationEmail rows, tightens read-in-app/email update filters, and refactors the resend email-opened webhook into a transactional flow.

Changes

Cohort / File(s) Summary
Notification schema updates
packages/prisma/schema/notification.prisma
Adds NotificationEmailType.Bounty and optional fields on NotificationEmail: bountyId, programId, partnerId, recipientUserId.
Bounty partner notifications
apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts
Fetches a partner user (take:1), captures sendBatchEmail response, and creates per-recipient notificationEmail records (type Bounty) with generated IDs and related context.
Partner message notifications
apps/web/app/(ee)/api/cron/messages/notify-partner/route.ts
Switches to partnerUsersToNotify (email,id), uses ordered unread messages (latest first), sends batch emails per-user, and persists per-user notificationEmail rows with generated IDs and references.
Program message notifications
apps/web/app/(ee)/api/cron/messages/notify-program/route.ts
Builds usersToNotify (email,id), checks latest unread via first element, sends per-user batch emails, and creates per-recipient notificationEmail entries with generated IDs and program/partner/recipient links.
ID generation
apps/web/lib/api/create-id.ts
Adds "em_" prefix option for notification email IDs while preserving existing ULID-like generation.
Resend email-opened webhook
apps/web/app/api/resend/webhook/email-opened.ts
Refactors to use a Prisma transaction callback (tx): sets openedAt on matching notificationEmail, finds that record, and bulk-updates message rows' readInEmail (guarding on missing program/partner). Logs transaction result.
Mark read in app (partners)
apps/web/lib/actions/partners/mark-partner-messages-read.ts
Adds readInApp: null filter to prisma.message.updateMany to only update messages not already read in-app.
Mark read in app (program)
apps/web/lib/actions/partners/mark-program-messages-read.ts
Adds readInApp: null filter to prisma.message.updateMany to only update messages not already read in-app.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Cron as Cron (bounties/messages)
  participant DB as Prisma DB
  participant Email as Resend (sendBatchEmail)

  rect rgb(240,248,255)
    note over Cron: Build usersToNotify and latest unread message context
    Cron->>DB: Query programs/partners, users, unread messages (ordered desc)
    alt Recipients exist
      Cron->>Email: sendBatchEmail(to: usersToNotify[].email, payloads)
      Email-->>Cron: { data: [{id: emailId}, ...] }
      Cron->>DB: createMany(notificationEmail entries per recipient: id em_..., type, programId/partnerId/bountyId, recipientUserId, emailId, messageId)
    else No recipients
      note over Cron: Exit
    end
  end
Loading
sequenceDiagram
  autonumber
  participant Resend as Resend
  participant API as Webhook /email-opened
  participant TX as Prisma Tx
  participant DB as DB

  Resend->>API: POST email-opened (emailId)
  API->>TX: run transaction
  rect rgb(245,255,245)
    TX->>DB: updateMany notificationEmail set openedAt (where openedAt null & emailId match)
    TX->>DB: findFirst notificationEmail by emailId
    alt notificationEmail found with programId & partnerId
      TX->>DB: updateMany messages set readInEmail (where readInEmail null, match programId/partnerId, createdAt <= notificationEmail.createdAt)
    else
      note over TX: No-op
    end
  end
  TX-->>API: result
Loading
sequenceDiagram
  autonumber
  participant UI as App UI
  participant API as mark-*-messages-read
  participant DB as Prisma DB

  UI->>API: Mark messages read (partner/program)
  API->>DB: updateMany messages (filters + readInApp: null) set readInApp=now
  DB-->>API: count updated
  API-->>UI: success
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Fix payout reminder cron #2812 — Modifies the same bounty notify cron handler and the batch email/notificationEmail persistence flow; strong overlap in logic changes.

Poem

A twitch of whiskers, em_ IDs in tow,
I thump the ground—batch emails go!
Per-user rows, ULIDs bright,
Opens marked in morning light.
I hop away, my work complete—rabbit trails and logs neat. 🐇✉️

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title clearly describes the primary feature of persisting bounty-related emails into the system by saving them to both Message and NotificationEmail, which directly matches the added NotificationEmail entries for bounties in the code changes. Although it does not mention every notification path, it is specific and concise about the main change.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bounties-messages

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 and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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 (1)
packages/prisma/schema/notification.prisma (1)

10-10: Index and FK for bountyId recommended.

You’ll likely filter/join by bountyId; add an index and (optionally) a FK to enforce integrity.

Add an index:

@@index([bountyId])

Optionally model the relation (if Bounty.id exists in the same schema):

bountyId String?
bounty   Bounty? @relation(fields: [bountyId], references: [id], onDelete: SetNull)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fcd7d99 and 521e073.

📒 Files selected for processing (4)
  • apps/web/app/(ee)/api/bounties/[bountyId]/route.ts (1 hunks)
  • apps/web/app/(ee)/api/bounties/route.ts (1 hunks)
  • apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts (6 hunks)
  • packages/prisma/schema/notification.prisma (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: TWilson023
PR: dubinc/dub#2736
File: apps/web/app/api/workspaces/[idOrSlug]/notification-preferences/route.ts:13-14
Timestamp: 2025-08-26T14:20:23.943Z
Learning: The updateNotificationPreference action in apps/web/lib/actions/update-notification-preference.ts already handles all notification preference types dynamically, including newBountySubmitted, through its schema validation using the notificationTypes enum and Prisma's dynamic field update pattern.
📚 Learning: 2025-08-26T14:20:23.943Z
Learnt from: TWilson023
PR: dubinc/dub#2736
File: apps/web/app/api/workspaces/[idOrSlug]/notification-preferences/route.ts:13-14
Timestamp: 2025-08-26T14:20:23.943Z
Learning: The updateNotificationPreference action in apps/web/lib/actions/update-notification-preference.ts already handles all notification preference types dynamically, including newBountySubmitted, through its schema validation using the notificationTypes enum and Prisma's dynamic field update pattern.

Applied to files:

  • packages/prisma/schema/notification.prisma
  • apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts
🧬 Code graph analysis (1)
apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts (3)
packages/email/src/index.ts (1)
  • sendBatchEmail (32-70)
apps/web/lib/api/create-id.ts (1)
  • createId (62-67)
packages/prisma/client.ts (1)
  • NotificationEmailType (14-14)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (8)
packages/prisma/schema/notification.prisma (1)

3-3: Enum addition LGTM.

New NotificationEmailType.Bounty is consistent with downstream usage.

apps/web/app/(ee)/api/bounties/[bountyId]/route.ts (1)

227-227: Propagating userId to cron: good.

Matches the cron’s schema (userId nullish) and preserves actor context.

Confirm the PATCH flow’s qstash trigger runs only when startsAt changes (guarded by the timestamp inequality) as intended for idempotency.

apps/web/app/(ee)/api/bounties/route.ts (1)

256-257: Include userId in creation notify payload: good.

Consistent with the update flow and cron handler.

apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts (5)

1-1: Import createId: OK.

Required for deterministic message IDs.


9-9: Using NotificationEmailType from Prisma: OK.

Enum aligns with schema changes.


18-20: Body schema accepts userId nullish: OK.

Allows defaulting to workspace owner in handler.


126-148: Batch email payload LGTM.

Idempotency-Key per page and template inputs look correct.

If Resend isn’t configured and SMTP fallback is used, sendBatchEmail returns { data: null }. Decide whether you still want to persist Message rows in that case; see next comment.


178-179: Propagate userId to next page: good.

Preserves actor context across pagination.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts (2)

132-154: Handle email provider errors explicitly.

Capture and check error from sendBatchEmail.

Apply:

-const { data } = await sendBatchEmail(
+const { data, error } = await sendBatchEmail(
   programEnrollments.map(({ partner }) => ({
     variant: "notifications",
     to: partner.email!,
     subject: `New bounty available for ${bounty.program.name}`,
     react: NewBountyAvailable({
       email: partner.email!,
       bounty: {
         name: bounty.name,
         type: bounty.type,
         endsAt: bounty.endsAt,
         description: bounty.description,
       },
       program: {
         name: bounty.program.name,
         slug: bounty.program.slug,
       },
     }),
     headers: {
       "Idempotency-Key": `${bountyId}-page-${page}`,
     },
   })),
 );
+if (error) {
+  throw new Error(
+    `Error sending bounty emails for bounty ${bountyId}: ${error.message}`,
+  );
+}

1-196: PR objective vs implementation: Messages not persisted.

The PR title/objective mentions persisting Bounties emails to Message + NotificationEmail. This route only writes NotificationEmail and never creates Message records.

I can propose a transactional insert to create Message per partner (with sender fallback to workspace owner or provided userId) and then NotificationEmail referencing those messageIds. Want me to draft it?

🧹 Nitpick comments (7)
packages/prisma/schema/notification.prisma (2)

10-16: Consider making emailId optional or model provider info for SMTP.

With SMTP fallback, sendBatchEmail returns data=null; current routes either skip persistence or throw. If you want consistent persistence, make emailId optional and add provider/providerMessageId fields, or persist records without provider message IDs.

Example schema adjustment (outside current diff context):

enum NotificationProvider {
  Resend
  SMTP
}

model NotificationEmail {
  id        String   @id @default(cuid())
  provider  NotificationProvider @default(Resend)
  emailId   String? // provider message id (nullable for SMTP)
  // ...
}

10-16: Index new lookup fields for expected queries.

If you’ll filter by bountyId/programId/partnerId/recipientUserId, add indexes.

Outside this hunk, append:

@@index([bountyId])
@@index([programId])
@@index([partnerId])
@@index([recipientUserId])
apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts (3)

44-60: Unneeded include increases query cost.

workspace.owner userId is fetched but unused. Remove unless you’ll use it for sender attribution when persisting Messages.


156-168: Guard against partial responses; skip duplicates.

Prevent mis-associating email IDs if provider returns fewer items. Also enable skipDuplicates (add a unique constraint on emailId to make it effective).

Apply:

-if (data) {
-  await prisma.notificationEmail.createMany({
-    data: programEnrollments.map(({ partner }, idx) => ({
-      id: createId({ prefix: "em_" }),
-      type: NotificationEmailType.Bounty,
-      emailId: data.data[idx].id,
-      bountyId: bounty.id,
-      programId: bounty.programId,
-      partnerId: partner.id,
-      recipientUserId: partner.users[0].userId, // TODO: update this to use partnerUsersToNotify approach
-    })),
-  });
-}
+if (data?.data?.length) {
+  if (data.data.length !== programEnrollments.length) {
+    console.warn(
+      `Partial email response for bounty ${bountyId}: expected ${programEnrollments.length}, got ${data.data.length}. Skipping persistence to avoid mis-association.`,
+    );
+  } else {
+    await prisma.notificationEmail.createMany({
+      data: programEnrollments.map(({ partner }, idx) => ({
+        id: createId({ prefix: "em_" }),
+        type: NotificationEmailType.Bounty,
+        emailId: data.data[idx].id,
+        bountyId: bounty.id,
+        programId: bounty.programId,
+        partnerId: partner.id,
+        recipientUserId: partner.users[0].userId, // TODO: update this to use partnerUsersToNotify approach
+      })),
+      skipDuplicates: true,
+    });
+  }
+}

9-10: Unify Prisma import path.

Other routes import NotificationEmailType from @dub/prisma/client. Prefer consistency to avoid multiple client instances in bundlers.

-import { NotificationEmailType } from "@prisma/client";
+import { NotificationEmailType } from "@dub/prisma/client";
apps/web/app/(ee)/api/cron/messages/notify-program/route.ts (1)

141-156: Gracefully handle SMTP fallback and partial responses.

Avoid throwing on data=null; persist only when provider returns IDs; guard for length mismatch; skip duplicates.

Apply:

-if (!data)
-  throw new Error(
-    `No data received from sending message emails to program ${programId} users`,
-  );
-
-await prisma.notificationEmail.createMany({
-  data: usersToNotify.map(({ id: userId }, idx) => ({
-    id: createId({ prefix: "em_" }),
-    type: NotificationEmailType.Message,
-    emailId: data.data[idx].id,
-    messageId: lastMessageId,
-    programId,
-    partnerId,
-    recipientUserId: userId,
-  })),
-});
+if (data?.data?.length) {
+  if (data.data.length !== usersToNotify.length) {
+    console.warn(
+      `Partial email response for program ${programId}: expected ${usersToNotify.length}, got ${data.data.length}. Skipping persistence to avoid mis-association.`,
+    );
+  } else {
+    await prisma.notificationEmail.createMany({
+      data: usersToNotify.map(({ id: userId }, idx) => ({
+        id: createId({ prefix: "em_" }),
+        type: NotificationEmailType.Message,
+        emailId: data.data[idx].id,
+        messageId: lastMessageId,
+        programId,
+        partnerId,
+        recipientUserId: userId,
+      })),
+      skipDuplicates: true,
+    });
+  }
+}
apps/web/app/(ee)/api/cron/messages/notify-partner/route.ts (1)

140-156: Gracefully handle SMTP fallback and partial responses.

Don’t throw when data is null; persist only when provider returns IDs; guard for length mismatch; skip duplicates.

Apply:

-if (!data)
-  throw new Error(
-    `No data received from sending message emails to partner ${partnerId}`,
-  );
-
-await prisma.notificationEmail.createMany({
-  data: partnerUsersToNotify.map(({ id: userId }, idx) => ({
-    id: createId({ prefix: "em_" }),
-    type: NotificationEmailType.Message,
-    emailId: data.data[idx].id,
-    messageId: lastMessageId,
-    programId,
-    partnerId,
-    recipientUserId: userId,
-  })),
-});
+if (data?.data?.length) {
+  if (data.data.length !== partnerUsersToNotify.length) {
+    console.warn(
+      `Partial email response for partner ${partnerId}: expected ${partnerUsersToNotify.length}, got ${data.data.length}. Skipping persistence to avoid mis-association.`,
+    );
+  } else {
+    await prisma.notificationEmail.createMany({
+      data: partnerUsersToNotify.map(({ id: userId }, idx) => ({
+        id: createId({ prefix: "em_" }),
+        type: NotificationEmailType.Message,
+        emailId: data.data[idx].id,
+        messageId: lastMessageId,
+        programId,
+        partnerId,
+        recipientUserId: userId,
+      })),
+      skipDuplicates: true,
+    });
+  }
+}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 521e073 and 3a971e8.

📒 Files selected for processing (5)
  • apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts (7 hunks)
  • apps/web/app/(ee)/api/cron/messages/notify-partner/route.ts (3 hunks)
  • apps/web/app/(ee)/api/cron/messages/notify-program/route.ts (3 hunks)
  • apps/web/lib/api/create-id.ts (1 hunks)
  • packages/prisma/schema/notification.prisma (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: TWilson023
PR: dubinc/dub#2736
File: apps/web/app/api/workspaces/[idOrSlug]/notification-preferences/route.ts:13-14
Timestamp: 2025-08-26T14:20:23.943Z
Learning: The updateNotificationPreference action in apps/web/lib/actions/update-notification-preference.ts already handles all notification preference types dynamically, including newBountySubmitted, through its schema validation using the notificationTypes enum and Prisma's dynamic field update pattern.
📚 Learning: 2025-08-26T14:20:23.943Z
Learnt from: TWilson023
PR: dubinc/dub#2736
File: apps/web/app/api/workspaces/[idOrSlug]/notification-preferences/route.ts:13-14
Timestamp: 2025-08-26T14:20:23.943Z
Learning: The updateNotificationPreference action in apps/web/lib/actions/update-notification-preference.ts already handles all notification preference types dynamically, including newBountySubmitted, through its schema validation using the notificationTypes enum and Prisma's dynamic field update pattern.

Applied to files:

  • apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts
  • packages/prisma/schema/notification.prisma
🧬 Code graph analysis (3)
apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts (3)
packages/email/src/index.ts (1)
  • sendBatchEmail (32-70)
apps/web/lib/api/create-id.ts (1)
  • createId (63-68)
packages/prisma/client.ts (1)
  • NotificationEmailType (14-14)
apps/web/app/(ee)/api/cron/messages/notify-partner/route.ts (4)
apps/web/app/(ee)/api/cron/utils.ts (1)
  • logAndRespond (1-13)
packages/email/src/index.ts (1)
  • sendBatchEmail (32-70)
apps/web/lib/api/create-id.ts (1)
  • createId (63-68)
packages/prisma/client.ts (1)
  • NotificationEmailType (14-14)
apps/web/app/(ee)/api/cron/messages/notify-program/route.ts (3)
apps/web/app/(ee)/api/cron/utils.ts (1)
  • logAndRespond (1-13)
packages/email/src/index.ts (1)
  • sendBatchEmail (32-70)
apps/web/lib/api/create-id.ts (1)
  • createId (63-68)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (4)
apps/web/lib/api/create-id.ts (1)

37-38: Add "em_" prefix looks good.

Prefix union stays type-safe and aligns with NotificationEmail IDs usage.

packages/prisma/schema/notification.prisma (1)

3-4: Enum addition is fine.

Adding Bounty to NotificationEmailType is backward-compatible for existing values.

apps/web/app/(ee)/api/cron/messages/notify-program/route.ts (1)

103-106: User list derivation looks good.

Maps to concrete user objects and filters nulls.

apps/web/app/(ee)/api/cron/messages/notify-partner/route.ts (1)

69-74: Match query filter to model field name
Search the Prisma schema for the exact field (notificationPreference vs notificationPreferences) and update the where clause in apps/web/app/(ee)/api/cron/messages/notify-partner/route.ts (lines 69–74) to use the correct field.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3a971e8 and abcbebe.

📒 Files selected for processing (5)
  • apps/web/app/(ee)/api/cron/messages/notify-partner/route.ts (4 hunks)
  • apps/web/app/(ee)/api/cron/messages/notify-program/route.ts (4 hunks)
  • apps/web/app/api/resend/webhook/email-opened.ts (1 hunks)
  • apps/web/lib/actions/partners/mark-partner-messages-read.ts (1 hunks)
  • apps/web/lib/actions/partners/mark-program-messages-read.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-17T17:44:03.965Z
Learnt from: TWilson023
PR: dubinc/dub#2857
File: apps/web/lib/actions/partners/update-program.ts:96-0
Timestamp: 2025-09-17T17:44:03.965Z
Learning: In apps/web/lib/actions/partners/update-program.ts, the team prefers to keep the messagingEnabledAt update logic simple by allowing client-provided timestamps rather than implementing server-controlled timestamp logic to avoid added complexity.

Applied to files:

  • apps/web/lib/actions/partners/mark-program-messages-read.ts
  • apps/web/lib/actions/partners/mark-partner-messages-read.ts
🧬 Code graph analysis (2)
apps/web/app/(ee)/api/cron/messages/notify-program/route.ts (4)
apps/web/app/(ee)/api/cron/utils.ts (1)
  • logAndRespond (1-13)
packages/email/src/index.ts (1)
  • sendBatchEmail (32-70)
apps/web/lib/api/create-id.ts (1)
  • createId (63-68)
packages/prisma/client.ts (1)
  • NotificationEmailType (16-16)
apps/web/app/(ee)/api/cron/messages/notify-partner/route.ts (4)
apps/web/app/(ee)/api/cron/utils.ts (1)
  • logAndRespond (1-13)
packages/email/src/index.ts (1)
  • sendBatchEmail (32-70)
apps/web/lib/api/create-id.ts (1)
  • createId (63-68)
packages/prisma/client.ts (1)
  • NotificationEmailType (16-16)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (4)
apps/web/lib/actions/partners/mark-partner-messages-read.ts (1)

24-33: Good call on gating by readInApp.

Filtering on readInApp: null keeps the update idempotent and avoids nuking existing read timestamps. Nice alignment with the program-side action.

apps/web/lib/actions/partners/mark-program-messages-read.ts (1)

25-34: Appreciate the symmetry with partner reads.

Adding the readInApp: null guard keeps this action consistent with the partner variant and prevents re-stamping already read messages.

apps/web/app/(ee)/api/cron/messages/notify-program/route.ts (1)

103-156: Per-recipient records look solid.

Love the shift to usersToNotify and the one-to-one notificationEmail inserts with generated IDs—this will make downstream correlation so much cleaner.

apps/web/app/(ee)/api/cron/messages/notify-partner/route.ts (1)

94-155: Nice mirror of the program-side flow.

The per-user batching plus createId usage keeps the partner notifications consistent with the program cron and sets us up for reliable webhook handling.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a7c02fe and b5a9465.

📒 Files selected for processing (1)
  • apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts (4 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-26T14:20:23.943Z
Learnt from: TWilson023
PR: dubinc/dub#2736
File: apps/web/app/api/workspaces/[idOrSlug]/notification-preferences/route.ts:13-14
Timestamp: 2025-08-26T14:20:23.943Z
Learning: The updateNotificationPreference action in apps/web/lib/actions/update-notification-preference.ts already handles all notification preference types dynamically, including newBountySubmitted, through its schema validation using the notificationTypes enum and Prisma's dynamic field update pattern.

Applied to files:

  • apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts
🧬 Code graph analysis (1)
apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts (3)
packages/email/src/index.ts (1)
  • sendBatchEmail (32-70)
apps/web/lib/api/create-id.ts (1)
  • createId (63-68)
packages/prisma/client.ts (1)
  • NotificationEmailType (16-16)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (1)
apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts (1)

112-114: Remove raw partner email addresses from logs.

Dumping individual partner emails into logs is PII leakage and violates our logging guidelines. Please log only aggregate counts/ids instead.

Apply this diff:

-    console.log(
-      `Sending emails to ${programEnrollments.length} partners: ${programEnrollments.map(({ partner }) => partner.email).join(", ")}`,
-    );
+    console.log(
+      `Sending emails to ${programEnrollments.length} partners.`,
+    );

Comment on lines +139 to +151
if (data) {
await prisma.notificationEmail.createMany({
data: programEnrollments.map(({ partner }, idx) => ({
id: createId({ prefix: "em_" }),
type: NotificationEmailType.Bounty,
emailId: data.data[idx].id,
bountyId: bounty.id,
programId: bounty.programId,
partnerId: partner.id,
recipientUserId: partner.users[0].userId, // TODO: update this to use partnerUsersToNotify approach
})),
});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Guard against partial Resend responses before indexing data.data[idx].

Resend’s batch API can return fewer entries or items with { error } instead of { id }. When that happens, data.data[idx] is undefined, so the current loop throws (can't read .id) and the whole cron crashes, skipping all remaining notifications. We need to skip errored recipients (and log them) before persisting NotificationEmail rows.

Apply this diff:

-    if (data) {
-      await prisma.notificationEmail.createMany({
-        data: programEnrollments.map(({ partner }, idx) => ({
-          id: createId({ prefix: "em_" }),
-          type: NotificationEmailType.Bounty,
-          emailId: data.data[idx].id,
-          bountyId: bounty.id,
-          programId: bounty.programId,
-          partnerId: partner.id,
-          recipientUserId: partner.users[0].userId, // TODO: update this to use partnerUsersToNotify approach
-        })),
-      });
-    }
+    if (data?.data?.length) {
+      const notificationEmails = [];
+
+      for (const [idx, enrollment] of programEnrollments.entries()) {
+        const emailResponse = data.data[idx];
+
+        if (!emailResponse?.id) {
+          await log({
+            message: `Resend did not return an id for partner ${enrollment.partner.id}; skipping NotificationEmail persistence.`,
+            type: "errors",
+          });
+          continue;
+        }
+
+        notificationEmails.push({
+          id: createId({ prefix: "em_" }),
+          type: NotificationEmailType.Bounty,
+          emailId: emailResponse.id,
+          bountyId: bounty.id,
+          programId: bounty.programId,
+          partnerId: enrollment.partner.id,
+          recipientUserId: enrollment.partner.users[0].userId, // TODO: update this to use partnerUsersToNotify approach
+        });
+      }
+
+      if (notificationEmails.length) {
+        await prisma.notificationEmail.createMany({
+          data: notificationEmails,
+        });
+      }
+    }
🤖 Prompt for AI Agents
In apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts around lines 139
to 151, guard against partial or errored Resend batch responses by checking
data.data for a valid entry with an id before indexing data.data[idx];
filter/map programEnrollments to only those where data.data[idx] exists and has
an id, log any missing/errored entries with context (partner id, idx, and any
error payload) and then call prisma.notificationEmail.createMany with the
filtered list; ensure recipientUserId remains populated as before and that
skipped recipients are not persisted so the cron does not crash on undefined
entries.

@steven-tey steven-tey merged commit 2e71627 into main Sep 25, 2025
7 of 8 checks passed
@steven-tey steven-tey deleted the bounties-messages branch September 25, 2025 00:46
@coderabbitai coderabbitai bot mentioned this pull request Sep 29, 2025
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

点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载