Skip to content

OUT-3928 | Send task-marked-as-done email notifications to IUs - #1372

Merged
arpandhakal merged 3 commits into
feature/iu-emailfrom
OUT-3928-send-task-marked-as-done-emails-to-ius
Jul 6, 2026
Merged

OUT-3928 | Send task-marked-as-done email notifications to IUs#1372
arpandhakal merged 3 commits into
feature/iu-emailfrom
OUT-3928-send-task-marked-as-done-emails-to-ius

Conversation

@arpandhakal

Copy link
Copy Markdown
Collaborator

Summary

Implements OUT-3928 — the IU-only "task marked as done" email. When a task is completed, IUs previously got only an in-product notification; this adds the email. Per the PRD's grouping rule ("group IU emails if >1 task notification in <5 minutes"), completions flow through the existing 5-minute grouped buffer using the reserved GroupedEmailEventType.COMPLETED.

Stacked on #1362 (OUT-3925) — base this PR on OUT-3925-extend-grouped-email-iu-recipients, review that one first.

What's in here

  • Templates: adds completion email copy for Completed, CompletedByIU, CompletedByCompanyMember, CompletedForCompanyByIU and maps all four to GroupedEmailEventType.COMPLETED in groupedEventTypeFor.
  • Flag gating: completion emails are gated on isIuEmailEnabled() (dev/staging only until OUT-3929 lands the platform preference), matching the assignment path.

Two regressions found in review and fixed here

  1. Client-actor completion poisoned the grouped email. The PRD's headline case is a client completing their own task. That buffers a row whose real sender is the client, but the flush hard-coded senderType: 'internalUser' — producing a senderId(client) + senderType(IU) mismatch that Copilot rejects. On retry exhaustion the failure handler deletes the entire window, losing unrelated buffered emails for that IU. Fix: sendGroupedEmail now carries the real actor's senderType/senderCompanyId from the buffered row (defaulting to IU only when no sender is present), plus the single-company senderCompanyId retry the individual-send path already has.

  2. Dedup guard dropped IU completion emails on client-assigned tasks. create()'s duplicate-notification guard is keyed on the client assignee but was gating the whole call, so CompletedByIU on a client-assigned task (recipient = creator IU) was skipped by the client's leftover assignment notification. Fix: the guard (and its query) now skip IU-recipient notifications.

A third, prospective issue — the ?? !email recipient inference in buildNotificationDetails becoming a landmine when OUT-3927 enables IU comment emails — is dormant in this ticket and deferred to OUT-3927.

Test plan

  • yarn tsc clean
  • notification + jobs suites green (139 tests), including new coverage for the client-actor sender passthrough, the single-company retry, and the dedup-guard skip
  • With IU_EMAIL_ALWAYS_ENABLED=true: client completes a task → creator IU receives the grouped/individual "task marked as done" email
  • Client completes 2+ tasks in 5 min → single grouped digest, delivered (no window wipe)
  • With the flag unset → no IU completion emails (prod-safe)

🤖 Generated with Claude Code

Adds the IU-only 'task marked as done' email (OUT-3928), routed through the
existing 5-minute grouped buffer per the PRD grouping rule.

- add completion email templates (Completed, CompletedByIU,
  CompletedByCompanyMember, CompletedForCompanyByIU) and map them to
  GroupedEmailEventType.COMPLETED
- gate completion emails on isIuEmailEnabled()
- carry the real actor's senderType/company through the grouped flush so a
  client-actor completion isn't sent as senderId(client)/senderType(IU), which
  Copilot rejects and which wipes the whole window on retry failure
- skip the client-notification dedup guard for IU recipients so CompletedByIU
  on a client-assigned task is no longer dropped by the client's leftover row

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

linear-code Bot commented Jul 3, 2026

Copy link
Copy Markdown

OUT-3928

@vercel

vercel Bot commented Jul 3, 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 6, 2026 10:05am

Request Review

@arpandhakal arpandhakal self-assigned this Jul 3, 2026
@arpandhakal
arpandhakal changed the base branch from OUT-3925-extend-grouped-email-iu-recipients to feature/iu-email July 3, 2026 12:10
@greptile-apps

greptile-apps Bot commented Jul 3, 2026

Copy link
Copy Markdown

Greptile Summary

This PR implements OUT-3928: IU-only "task marked as done" email notifications, routing all four completion actions through the existing 5-minute grouped email buffer. It also fixes two regressions found during review — a client-actor sender mismatch that could poison grouped emails, and a dedup guard that was incorrectly blocking IU-recipient completion notifications on client-assigned tasks.

  • Email templates: Adds a shared completedDetail template for Completed, CompletedByIU, CompletedByCompanyMember, and CompletedForCompanyByIU, all mapped to GroupedEmailEventType.COMPLETED.
  • Sender context fix: senderFromEvents and sendGroupedEmail now carry the real actor's senderType/senderCompanyId from the buffered row, preventing Copilot from rejecting grouped emails when a client is the actor. A single-company senderCompanyId retry is also added to sendGroupedEmail, mirroring the retry already in NotificationService.
  • Dedup guard fix: create()'s client-notification dedup guard is now skipped for IU-recipient notifications via the new isCompletionAction() predicate, which is also shared with groupedEventTypeFor() to keep the two in sync.

Confidence Score: 5/5

Safe to merge — the completion email paths are flag-gated on isIuEmailEnabled() (dev/staging only), the two regressions fixed here are well-reasoned and tested, and no live call site is affected by the minor inconsistency in createBulkNotification.

All completion actions are correctly routed to IU recipients, the grouped-email buffer carries the full sender identity from buffered rows, the dedup guard no longer blocks IU-recipient notifications on client-assigned tasks, and the flag gates ensure no production impact. Test coverage is thorough across all four completion actions and the new grouped-email sender passthrough. The only gap is a latent inconsistency in createBulkNotification's inline isRecipientIu check that doesn't use isCompletionAction(), but it doesn't affect any current caller.

notification.service.ts — the createBulkNotification method has a manually-inlined completion check that covers only two of the four completion actions, diverging from the isCompletionAction() predicate used in create().

Important Files Changed

Filename Overview
src/app/api/notification/notification.service.ts Adds isCompletionAction() predicate used by create() for correct IU routing and dedup-guard bypass; createBulkNotification has a parallel inline check that only covers 2 of 4 completion actions but is correct for all current callers.
src/jobs/notifications/flush-grouped-email.ts Extends senderFromEvents to return the full sender context (type + company) from the same buffered event object, ensuring grouped emails carry a consistent senderType/senderCompanyId when the actor is a client.
src/jobs/notifications/send-grouped-email.ts Adds optional senderType and senderCompanyId params; defaults senderType to 'internalUser'; adds the same senderCompanyId retry logic already present in NotificationService for single-company workspaces.
src/app/api/tasks/task-notifications.service.ts Gates completion emails on isIuEmailEnabled() across all four completion paths and threads email: isIuEmailEnabled() into both createBulkNotification calls.
src/app/api/notification/notification.helpers.ts Adds completedDetail template shared across all four completion actions, replacing the previously commented-out stub with proper subject, header, body, and ctaParams.
src/app/api/notification/notification.service.test.ts New guard: IU completion emails suite covers all four completion actions, dedup-guard bypass for IU recipient on client-assigned tasks, bulk routing, and flag-off no-op path.
src/jobs/notifications/send-grouped-email.test.ts Two new tests validate client-actor sender passthrough and single-company senderCompanyId retry for grouped emails.
src/app/api/notification/notification.helpers.test.ts Extends the template-coverage test to include all four completion actions and adds a copy-accuracy test verifying subject, header, body, and title for completion emails.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Task marked complete] --> B{Who completed it?}
    B -->|IU| C{Assignee type?}
    B -->|Client| D{Assignee type?}

    C -->|company| E[CompletedForCompanyByIU\ncreate]
    C -->|client or IU| F[CompletedByIU\ncreate]
    D -->|company| G[CompletedByCompanyMember\ncreateBulkNotification]
    D -->|other| H[Completed\ncreateBulkNotification]

    E & F --> I{isIuEmailEnabled?}
    G & H --> I

    I -->|No| J[In-product only]
    I -->|Yes| K[bufferGroupedEmailEvent\nrecipientIuId set]

    K --> L[GroupedEmailEvents DB row\neventType=COMPLETED\nindividualEmail carries\nsenderId + senderType + senderCompanyId]
    L --> M[flush-grouped-email job\n5-min delay]
    M --> N{1 live event?}
    N -->|Yes| O[sendIndividualEmail\noriginal payload verbatim]
    N -->|No| P[senderFromEvents\npicks first event sender]
    P --> Q[sendGroupedEmail\nwith real senderType/senderCompanyId]
    Q --> R{Copilot rejects\nsenderCompanyId?}
    R -->|Yes| S[Retry without\nsenderCompanyId]
    R -->|No| T[IU receives\ngrouped digest email]
    S --> T
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"}}}%%
flowchart TD
    A[Task marked complete] --> B{Who completed it?}
    B -->|IU| C{Assignee type?}
    B -->|Client| D{Assignee type?}

    C -->|company| E[CompletedForCompanyByIU\ncreate]
    C -->|client or IU| F[CompletedByIU\ncreate]
    D -->|company| G[CompletedByCompanyMember\ncreateBulkNotification]
    D -->|other| H[Completed\ncreateBulkNotification]

    E & F --> I{isIuEmailEnabled?}
    G & H --> I

    I -->|No| J[In-product only]
    I -->|Yes| K[bufferGroupedEmailEvent\nrecipientIuId set]

    K --> L[GroupedEmailEvents DB row\neventType=COMPLETED\nindividualEmail carries\nsenderId + senderType + senderCompanyId]
    L --> M[flush-grouped-email job\n5-min delay]
    M --> N{1 live event?}
    N -->|Yes| O[sendIndividualEmail\noriginal payload verbatim]
    N -->|No| P[senderFromEvents\npicks first event sender]
    P --> Q[sendGroupedEmail\nwith real senderType/senderCompanyId]
    Q --> R{Copilot rejects\nsenderCompanyId?}
    R -->|Yes| S[Retry without\nsenderCompanyId]
    R -->|No| T[IU receives\ngrouped digest email]
    S --> T
Loading

Reviews (3): Last reviewed commit: "Merge branch 'feature/iu-email' into OUT..." | Re-trigger Greptile

Comment on lines +39 to +46
const isAssignedToIu =
task.assigneeType === AssigneeType.internalUser &&
(action === NotificationTaskActions.Assigned || action === NotificationTaskActions.ReassignedToIU)
// Completion notifications go to the task creator, an IU
const isRecipientIu =
isAssignedToIu ||
action === NotificationTaskActions.CompletedByIU ||
action === NotificationTaskActions.CompletedForCompanyByIU

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Completed absent from isRecipientIu in create()

groupedEventTypeFor now maps NotificationTaskActions.Completed to GroupedEmailEventType.COMPLETED, but isRecipientIu in create() does not include Completed. No current call site passes Completed to create() (it always goes through createBulkNotification), so this is not a live bug — but if a future caller invokes create(Completed, task), isRecipientIu will be false, causing bufferGroupedEmailEvent to write recipientClientId = task.createdById (an IU ID) instead of recipientIuId, and buildNotificationDetails to produce recipientClientId in the individual email. The flush would then group those rows as CU events and silently mis-deliver them. Adding action === NotificationTaskActions.Completed to the isRecipientIu guard here would make the two methods consistent and safe against future callers.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch — fixed in 8403c94. Extracted a single isCompletionAction predicate now used by both create()'s isRecipientIu and groupedEventTypeFor, so the two can't drift, and create() now routes all four Completed* actions as IU. Also extended the completion test to cover all four so a future create(Completed) misroute would fail the suite.

…o IU

Greptile flagged that groupedEventTypeFor maps all four Completed* actions to
COMPLETED, but create()'s isRecipientIu only covered CompletedByIU and
CompletedForCompanyByIU. Latent today (Completed/CompletedByCompanyMember only
reach createBulkNotification), but a future create(Completed) call would buffer
the IU recipient as a CU row and mis-deliver.

Extract a single isCompletionAction predicate used by both create() and
groupedEventTypeFor so the two can't drift, and extend the completion test to
cover all four actions.

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

vercel Bot commented Jul 6, 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

@arpandhakal

Copy link
Copy Markdown
Collaborator Author

@greptile please re review.

@arpandhakal

Copy link
Copy Markdown
Collaborator Author

@greptile review again please.

@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 merged commit 005a42f into feature/iu-email Jul 6, 2026
4 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.

2 participants