Skip to content

OUT-3925, OUT-3926 | Extend grouped email system to support IU recipients - #1362

Merged
arpandhakal merged 7 commits into
feature/iu-emailfrom
OUT-3925-extend-grouped-email-iu-recipients
Jul 3, 2026
Merged

OUT-3925, OUT-3926 | Extend grouped email system to support IU recipients#1362
arpandhakal merged 7 commits into
feature/iu-emailfrom
OUT-3925-extend-grouped-email-iu-recipients

Conversation

@arpandhakal

@arpandhakal arpandhakal commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Wires recipientIuId into bufferGroupedEmailEvent with a separate window query and IU-specific window key format ({iuId}:iu:{uuid}) to avoid collisions with CU keys
  • Teaches flushGroupedEmail to group and flush IU buffer rows, sending to recipientInternalUserId in the Copilot notification payload
  • Adds isIuEmailEnabled() stub backed by IU_EMAIL_ALWAYS_ENABLED env var so IU emails can be tested locally before OUT-3929 (platform preference flag) ships
  • Removes the hard disableEmail block for IU assignees in sendUserTaskNotification, gating it on isIuEmailEnabled() instead
  • Fixes buildNotificationDetails to accept an explicit isIuRecipient flag so the payload correctly sets recipientInternalUserId (the old inference from absence-of-email breaks once IUs have email)

Test plan

  • Set IU_EMAIL_ALWAYS_ENABLED=true in .env.local
  • Assign a task to an IU — confirm a GroupedEmailEvents row is written with recipientIuId set and recipientClientId null
  • Wait 5 minutes (or trigger flushGroupedEmail manually) — confirm the email is sent to the IU via recipientInternalUserId
  • Assign multiple tasks to the same IU within 5 minutes — confirm a single grouped summary email is sent
  • Assign a task to a CU — confirm existing CU grouped email behaviour is unchanged
  • With IU_EMAIL_ALWAYS_ENABLED unset — confirm no IU emails are sent (prod-safe)

🤖 Generated with Claude Code

…ents

- Wire recipientIuId into bufferGroupedEmailEvent with a separate window
  query and IU-specific window key (avoids collisions with CU keys)
- Teach flushGroupedEmail to group and flush IU recipient rows via
  recipientInternalUserId in the Copilot notification payload
- Add isIuEmailEnabled() stub backed by IU_EMAIL_ALWAYS_ENABLED env var
  so IU emails can be tested before OUT-3929 (platform preference flag) ships
- Remove disableEmail hard-block for IU assignees; gate on isIuEmailEnabled()
- Fix buildNotificationDetails to accept explicit isIuRecipient flag so
  the payload sets recipientInternalUserId correctly when IU has email

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Jun 30, 2026

Copy link
Copy Markdown

OUT-3925

OUT-3926

@vercel

vercel Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
tasks-app Ready Ready Preview, Comment Jul 3, 2026 10:18am

Request Review

@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown

Greptile Summary

This PR extends the grouped email buffering system to support Internal User (IU) recipients, gated behind an IU_EMAIL_ALWAYS_ENABLED env flag while the platform preference check (OUT-3929) is pending.

  • Wires recipientIuId into bufferGroupedEmailEvent with a separate IU window query and a collision-safe key format ({iuId}:iu:{uuid}); adds groupIuRecipients / markIuRecipientSent / an IU flush loop to flushGroupedEmail that mirrors the existing CU path.
  • Adds ReassignedToIU to groupedEventTypeFor and isIuRecipient, fixes buildNotificationDetails to accept an explicit flag (replacing the fragile "no-email → must be IU" inference), and removes the hard disableEmail block for IU assignees in favour of the feature flag.

Confidence Score: 5/5

Safe to merge; the IU email path is entirely behind the IU_EMAIL_ALWAYS_ENABLED flag so prod behaviour is unchanged until explicitly opted in.

The IU flush loop faithfully mirrors the existing CU loop, the window-key namespace prevents collisions, the feature flag correctly gates all buffering through disableEmail, and the three new guard tests cover the key wiring boundaries. The only open item is a type-safety gap in SendGroupedEmailArgs that does not affect any current call site.

src/jobs/notifications/send-grouped-email.ts — the optional recipient fields have no compile-time enforcement that at least one is provided.

Important Files Changed

Filename Overview
src/app/api/notification/isIuEmailEnabled.ts New feature-flag helper wrapping iuEmailAlwaysEnabled from config; trivially correct.
src/config/index.ts Adds iuEmailAlwaysEnabled boolean parsed from IU_EMAIL_ALWAYS_ENABLED env var; straightforward and prod-safe (defaults false).
src/app/api/tasks/task-notifications.service.ts Replaces hard disableEmail for IU assignees with a flag-gated condition; logic is correct and prod-safe when flag is unset.
src/app/api/notification/notification.service.ts Adds isIuRecipient derivation, IU-aware buffer routing, ReassignedToIU in groupedEventTypeFor, and explicit isIuRecipient flag in buildNotificationDetails; logic is consistent and the old inference fallback is preserved for unupdated paths.
src/jobs/notifications/flush-grouped-email.ts Adds parallel IU flush loop with groupIuRecipients, markIuRecipientSent, and IU-specific Sentry breadcrumbs; mirrors the CU loop faithfully. Type in SendGroupedEmailArgs (see inline comment) is the only concern.
src/jobs/notifications/send-grouped-email.ts Extends SendGroupedEmailArgs with optional recipientInternalUserId; both recipient fields are now optional with no compile-time enforcement that at least one is present.
src/app/api/notification/notification.service.test.ts Adds three IU wiring guard tests (buffer row shape, in-product routing, disableEmail guard) and updates the groupedEventTypeFor guard for ReassignedToIU; good coverage of the new paths.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant TN as TaskNotificationsService
    participant NS as NotificationService
    participant DB as GroupedEmailEvents (DB)
    participant TQ as Trigger.dev Queue
    participant FG as flushGroupedEmail (job)
    participant CP as Copilot API

    TN->>NS: "create(Assigned/ReassignedToIU, task, {disableEmail: false})"
    NS->>NS: "isIuRecipient = true"
    NS->>NS: "groupedType = ASSIGNED"
    NS->>DB: bufferGroupedEmailEvent (recipientIuId, windowKey: iuId:iu:uuid)
    NS->>TQ: enqueueGroupedEmailFlush (delay 5m)
    NS->>CP: createNotification (inProduct only)

    Note over TQ,FG: 5 minutes later
    TQ->>FG: "flushGroupedEmailRun({workspaceId, windowKey})"
    FG->>DB: readUnsentWindowEvents
    FG->>FG: groupIuRecipients(rows)
    alt single live event
        FG->>CP: sendIndividualEmail (recipientInternalUserId)
    else multiple live events
        FG->>CP: sendGroupedEmail (recipientInternalUserId)
    end
    FG->>DB: markIuRecipientSent
    FG->>DB: deleteWindowRows (sentAt set)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant TN as TaskNotificationsService
    participant NS as NotificationService
    participant DB as GroupedEmailEvents (DB)
    participant TQ as Trigger.dev Queue
    participant FG as flushGroupedEmail (job)
    participant CP as Copilot API

    TN->>NS: "create(Assigned/ReassignedToIU, task, {disableEmail: false})"
    NS->>NS: "isIuRecipient = true"
    NS->>NS: "groupedType = ASSIGNED"
    NS->>DB: bufferGroupedEmailEvent (recipientIuId, windowKey: iuId:iu:uuid)
    NS->>TQ: enqueueGroupedEmailFlush (delay 5m)
    NS->>CP: createNotification (inProduct only)

    Note over TQ,FG: 5 minutes later
    TQ->>FG: "flushGroupedEmailRun({workspaceId, windowKey})"
    FG->>DB: readUnsentWindowEvents
    FG->>FG: groupIuRecipients(rows)
    alt single live event
        FG->>CP: sendIndividualEmail (recipientInternalUserId)
    else multiple live events
        FG->>CP: sendGroupedEmail (recipientInternalUserId)
    end
    FG->>DB: markIuRecipientSent
    FG->>DB: deleteWindowRows (sentAt set)
Loading

Reviews (2): Last reviewed commit: "fix(notifications): buffer ReassignedToI..." | Re-trigger Greptile

Comment thread src/app/api/notification/notification.service.test.ts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Deployment failed with the following error:

Deploying Serverless Functions to multiple regions is restricted to the Pro and Enterprise plans.

Learn More: https://vercel.link/multiple-function-regions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@arpandhakal

Copy link
Copy Markdown
Collaborator Author

please review again @greptile

Comment thread src/app/api/notification/notification.service.ts Outdated
Comment thread src/app/api/notification/notification.service.ts Outdated
Comment thread src/config/index.ts
…ipients

Address PR review: pass recipient ids via conditional spread instead of
explicit null, and drop the null type from bufferGroupedEmailEvent params.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rbitrary IU

The flush job attributed grouped emails to resolveSenderId (an arbitrary
workspace internal user). Copilot creates the notification but does not
deliver the email to an IU recipient from an unrelated sender, so IU grouped
emails (event count > 1) silently failed while single-event and client
grouped emails worked. Use the sender captured on the buffered individualEmail
and fall back to resolveSenderId only for pre-migration rows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
getEmailDetails had no ReassignedToIU entry, so reassignment to an IU
produced no email and was never buffered into the grouped queue — only
the in-product notification fired. Add the template (same copy as
ReassignedToClient) and a regression test asserting IU-recipient
actions have email templates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread src/app/api/notification/isIuEmailEnabled.ts
Comment thread src/app/api/notification/notification.service.ts Outdated
Comment thread src/app/api/notification/notification.service.ts Outdated
Comment thread src/app/api/notification/notification.service.ts Outdated
Comment thread src/app/api/notification/notification.service.ts Outdated
- rename isIuRecipient to isAssignedToIu / isRecipientIu for clarity
- bufferGroupedEmailEvent now takes recipientId + companyId + isRecipientIu
  and routes to the right column internally
- collapse the duplicated window queries into one with a recipient filter fragment

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@priosshrsth priosshrsth left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

lgtm

@arpandhakal arpandhakal changed the title OUT-3925 | Extend grouped email system to support IU recipients OUT-3925, OUT-3926 | Extend grouped email system to support IU recipients Jul 3, 2026
@arpandhakal
arpandhakal merged commit 1c04407 into feature/iu-email Jul 3, 2026
3 checks passed
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.

3 participants