Skip to content

feat(emails): confirm a new schedule to the account holder (chat#1889 row 14) - #788

Open
sweetmantech wants to merge 6 commits into
mainfrom
feat/schedule-confirmation-email
Open

feat(emails): confirm a new schedule to the account holder (chat#1889 row 14)#788
sweetmantech wants to merge 6 commits into
mainfrom
feat/schedule-confirmation-email

Conversation

@sweetmantech

@sweetmantech sweetmantech commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Matrix row 14 in recoupable/chat#1889 — the last genuine gap in the email journey. Stacked on #784 (shared email layout), which the issue lists as its blocker. Review/merge after #784.

Why

A signup finishes onboarding, schedules a report, and then hears nothing until a report lands days later. There is no confirmation that anything was actually scheduled, and no record of when it will arrive — the weakest link between signup and the first delivered value.

What changed

  • buildScheduleConfirmationEmail — subject Scheduled: <title>, body naming the report and its cadence, CTA to /tasks. Renders through the shared renderEmailLayout so it reads as one family with the welcome, valuation, and weekly-report emails.
  • describeCronCadence — turns 0 13 * * 1 into Mondays at 13:00 UTC, honouring an explicit IANA zone when the task has one. Deliberately narrow: it describes only the fixed-weekday and fixed-daily shapes onboarding actually creates, and otherwise falls back to the schedule <cron>. An honest cron string in an email beats a confidently wrong sentence about when someone's report will arrive.
  • sendScheduleConfirmationEmail — fires from createTaskHandler after the schedule is materialized (the same rule the welcome and valuation emails follow), deduped per task id via a "task_id":"…" marker in email_send_log.raw_body, and swallows its own failures so a Resend or DB problem can never fail task creation. A wallet/phone-only login with no address is skipped silently.

Verification

TDD, red → green on both new units (each RED as "Cannot find module", then green):

```
✓ lib/emails/tests/buildScheduleConfirmationEmail.test.ts (4 tests)
✓ lib/emails/tests/describeCronCadence.test.ts (4 tests)
```

createTaskHandler.test.ts gained two behavioural tests — that a successful create does confirm with the right payload, and that a failed create does not send. Adding the send also surfaced that this suite loaded the Resend client transitively (RESEND_API_KEY is not configured); it now mocks the sender, mirroring how createAccountHandler.test.ts mocks sendWelcomeEmail.

```
pnpm exec vitest run lib/tasks lib/emails
Test Files 49 passed (49)
Tests 285 passed (285)

pnpm exec vitest run # full suite, no regressions
Test Files 786 passed (786)
```

pnpm exec tsc --noEmit clean for the touched files (repo baseline of 236 pre-existing errors in lib/trigger/__tests__ is unchanged).

Live send verification pending.

Tracked in chat#1889 (matrix row 14).


Summary by cubic

Adds a schedule confirmation email after task creation so users know what was scheduled and when. Introduces a shared email layout adopted by welcome, valuation, and weekly-report emails for a consistent look; addresses chat#1889 (row 14).

  • New Features

    • Schedule confirmation email: subject "Scheduled: <title>", cadence from cron (falls back to raw cron), CTA to /tasks.
    • Triggered post-creation; deduped per task via email_send_log; skips accounts without an email; never blocks task creation.
    • Shared renderEmailLayout used by welcome and valuation builders, and by weekly reports via processAndSendEmail.
  • Bug Fixes

    • Font stack uses single quotes to avoid breaking style attributes in email clients.

Written for commit a2e00d5. Summary will update on new commits.

Review in cubic

sweetmantech and others added 5 commits July 23, 2026 06:28
Welcome, valuation, and weekly-report emails should share one visual language
(DESIGN.md — four-font system, shadow-as-border, achromatic chrome), but each
outbound email was assembled ad hoc (body + bare footer). This establishes the
shared template (recoupable/chat#1885 consistency pass).

- `renderEmailLayout` (`lib/emails/`) — one header + footer + optional-CTA
  wrapper: Recoup wordmark header, achromatic shadow-as-border card, the
  DESIGN.md font stack, centered fixed-max-width container. Email-client-safe
  (inline styles, literal hex tokens, webfonts degrade to system fonts).
- `processAndSendEmail` — adopts the wrapper as the proof point. This is the
  path the live weekly-report email flows through (agent `send_email` →
  `processAndSendEmail`), so the already-live weekly report now renders in the
  shared house style, with the existing footer carried in as the layout footer.

Welcome (api#774) and valuation (api#773) emails adopt the same wrapper once
merged — this PR establishes the shared template.

TDD: RED→GREEN for the layout renderer (body/footer/CTA + house-style markers)
and for the processAndSendEmail adoption.

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

Completes the consistency pass — previously the wrapper was only adopted in the
weekly-report path, so welcome/valuation still shipped their own chrome.

- buildWelcomeEmail + renderValuationReportHtml now render through
  renderEmailLayout (body + cta + footer), dropping their duplicated outer
  page/card chrome; each keeps only its own content.
- Fix: FONT_STACK used double-quoted font names inside double-quoted
  style="…" attributes, which terminated the attribute early — silently
  dropping the font (clients fell back to serif) AND every declaration after
  it (the CTA button lost its background/padding/radius). Single-quote the
  font names; add a regression test.
- Layout footer tagline: em dash -> comma (house copy rule).
- Consistency assertions added to both email tests.

199 lib/emails tests pass; lint + format clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
chat#1889 matrix row 14 — the last genuine gap in the email journey.

A signup finished onboarding and then heard nothing until a report arrived days
later, with no record that anything had actually been scheduled.

- New buildScheduleConfirmationEmail renders through the shared renderEmailLayout
  (api#784), so it reads as one family with welcome/valuation/weekly-report.
- New describeCronCadence turns the cron into "Mondays at 13:00 UTC",
  deliberately narrow: it only describes the fixed weekday/daily shapes
  onboarding creates and otherwise falls back to the raw expression, since an
  honest cron string beats a confidently wrong sentence about delivery time.
- New sendScheduleConfirmationEmail fires from createTaskHandler after the
  schedule is materialized, deduped per task id in email_send_log, and swallows
  its own failures so it can never fail task creation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
api Ready Ready Preview Jul 27, 2026 2:20pm

Request Review

@cursor

cursor Bot commented Jul 27, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Warning

Review limit reached

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

Next review available in: 58 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c79e6ef9-b3b4-4c89-8656-f8d0a67595ff

📥 Commits

Reviewing files that changed from the base of the PR and between 96959eb and a2e00d5.

⛔ Files ignored due to path filters (7)
  • lib/emails/__tests__/buildScheduleConfirmationEmail.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/emails/__tests__/buildWelcomeEmail.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/emails/__tests__/describeCronCadence.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/emails/__tests__/processAndSendEmail.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/emails/__tests__/renderEmailLayout.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/emails/valuationReport/__tests__/renderValuationReportHtml.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/tasks/__tests__/createTaskHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (9)
  • lib/const.ts
  • lib/emails/buildScheduleConfirmationEmail.ts
  • lib/emails/buildWelcomeEmail.ts
  • lib/emails/describeCronCadence.ts
  • lib/emails/processAndSendEmail.ts
  • lib/emails/renderEmailLayout.ts
  • lib/emails/sendScheduleConfirmationEmail.ts
  • lib/emails/valuationReport/renderValuationReportHtml.ts
  • lib/tasks/createTaskHandler.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/schedule-confirmation-email

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.

@cubic-dev-ai cubic-dev-ai 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.

2 issues found across 16 files

Confidence score: 5/5

  • In lib/emails/sendScheduleConfirmationEmail.ts, mixing recipient/dedup resolution with delivery/logging increases maintenance risk and makes behavior changes easier to regress in future email updates—split these concerns into focused email-domain helpers to keep side effects isolated.
  • In lib/emails/renderEmailLayout.ts, keeping header, CTA, footer, and shell markup in one large renderer makes template evolution and targeted edits harder, which can lead to accidental cross-section layout breakage—extract each section into dedicated helpers/modules and keep this function as a thin composer.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="lib/emails/sendScheduleConfirmationEmail.ts">

<violation number="1" location="lib/emails/sendScheduleConfirmationEmail.ts:25">
P3: This 66-line function exceeds the repository's 20-line SRP limit and coordinates several separate responsibilities. Split recipient/dedup resolution and delivery/logging into focused email-domain functions.</violation>
</file>

<file name="lib/emails/renderEmailLayout.ts">

<violation number="1" location="lib/emails/renderEmailLayout.ts:51">
P3: Shared layout renderer exceeds repository’s 20-line SRP limit, making header, CTA, footer, and shell markup harder to evolve independently. Extract the sections into focused helpers/modules, leaving this function as composition.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Client as Client (UI/API)
    participant Handler as createTaskHandler
    participant EmailModule as sendScheduleConfirmationEmail
    participant EmailBuilder as buildScheduleConfirmationEmail
    participant Cadence as describeCronCadence
    participant Layout as renderEmailLayout
    participant DB as Database
    participant Resend as Resend API

    Note over Client,Resend: Schedule Confirmation Email Flow

    Client->>Handler: POST /api/tasks (create schedule)
    Handler->>Handler: validateCreateTaskRequest()
    Handler->>DB: createTask()
    DB-->>Handler: createdTask { id, title, schedule, timezone }

    Note over Handler: NEW: Fire confirmation after materialization
    Handler->>EmailModule: sendScheduleConfirmationEmail(accountId, taskId, title, schedule, timeZone)

    EmailModule->>DB: selectEmailSendLog(status="sent", rawBody LIKE task_id)
    alt Already sent (idempotent)
        DB-->>EmailModule: Found existing log
        EmailModule-->>Handler: return (skip)
    else Not yet sent
        DB-->>EmailModule: No matching log
        EmailModule->>DB: selectAccountEmails(accountIds)
        alt No email address
            DB-->>EmailModule: null
            EmailModule-->>Handler: return (skip silently)
        else Has email
            DB-->>EmailModule: accountEmail

            EmailModule->>Cadence: describeCronCadence(schedule, timeZone)
            alt Fixed weekday pattern
                Cadence-->>EmailModule: "Mondays at 13:00 UTC"
            else Fixed daily pattern
                Cadence-->>EmailModule: "every day at 08:00 UTC"
            else Unrecognized pattern
                Cadence-->>EmailModule: "the schedule <cron_expression>"
            end

            EmailModule->>EmailBuilder: buildScheduleConfirmationEmail(title, cadence)
            EmailBuilder->>Layout: renderEmailLayout(bodyHtml, cta, footerHtml)
            Layout-->>EmailBuilder: Wrapped HTML (header + card + footer)
            EmailBuilder-->>EmailModule: { subject, html }

            EmailModule->>Resend: sendEmailWithResend(to, subject, html)
            
            alt Send success
                Resend-->>EmailModule: { id }
                EmailModule->>DB: logEmailAttempt(status="sent", resendId)
            else Send failure
                Resend-->>EmailModule: NextResponse error
                EmailModule->>DB: logEmailAttempt(status="send_failed")
            end

            Note over EmailModule: Swallows all errors (console.error)
            EmailModule-->>Handler: void (best-effort, never throws)
        end
    end

    Handler-->>Client: 200 { status: "success", task }

    Note over Client,Resend: Error Path
    
    Client->>Handler: POST /api/tasks
    Handler->>DB: createTask()
    DB-->>Handler: REJECTED (trigger failure)
    Note over Handler: Schedule NOT created
    Handler-->>Client: 500 error
    Note over Handler: No confirmation email sent (verified in tests)
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

* create can't double-send. Best-effort: never throws, so a Resend or DB
* failure can never fail task creation.
*/
export async function sendScheduleConfirmationEmail({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: This 66-line function exceeds the repository's 20-line SRP limit and coordinates several separate responsibilities. Split recipient/dedup resolution and delivery/logging into focused email-domain functions.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/emails/sendScheduleConfirmationEmail.ts, line 25:

<comment>This 66-line function exceeds the repository's 20-line SRP limit and coordinates several separate responsibilities. Split recipient/dedup resolution and delivery/logging into focused email-domain functions.</comment>

<file context>
@@ -0,0 +1,90 @@
+ * create can't double-send. Best-effort: never throws, so a Resend or DB
+ * failure can never fail task creation.
+ */
+export async function sendScheduleConfirmationEmail({
+  accountId,
+  taskId,
</file context>

* Email-client-safe: all styles inline, a centered fixed-max-width container,
* literal hex tokens (no CSS variables), and webfonts degrade to system fonts.
*/
export function renderEmailLayout({ bodyHtml, footerHtml, cta }: RenderEmailLayoutParams): string {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: Shared layout renderer exceeds repository’s 20-line SRP limit, making header, CTA, footer, and shell markup harder to evolve independently. Extract the sections into focused helpers/modules, leaving this function as composition.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/emails/renderEmailLayout.ts, line 51:

<comment>Shared layout renderer exceeds repository’s 20-line SRP limit, making header, CTA, footer, and shell markup harder to evolve independently. Extract the sections into focused helpers/modules, leaving this function as composition.</comment>

<file context>
@@ -0,0 +1,88 @@
+ * Email-client-safe: all styles inline, a centered fixed-max-width container,
+ * literal hex tokens (no CSS variables), and webfonts degrade to system fonts.
+ */
+export function renderEmailLayout({ bodyHtml, footerHtml, cta }: RenderEmailLayoutParams): string {
+  const header = `
+<div style="padding:0 0 20px;">
</file context>

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai 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.

0 issues found across 4 files (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Requires human review: Auto-approval blocked by 2 unresolved issues from previous reviews.

Re-trigger cubic

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