feat(emails): confirm a new schedule to the account holder (chat#1889 row 14) - #788
feat(emails): confirm a new schedule to the account holder (chat#1889 row 14)#788sweetmantech wants to merge 6 commits into
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
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. |
|
Warning Review limit reached
Next review available in: 58 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (7)
📒 Files selected for processing (9)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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)
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({ |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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
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— subjectScheduled: <title>, body naming the report and its cadence, CTA to/tasks. Renders through the sharedrenderEmailLayoutso it reads as one family with the welcome, valuation, and weekly-report emails.describeCronCadence— turns0 13 * * 1intoMondays 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 tothe schedule <cron>. An honest cron string in an email beats a confidently wrong sentence about when someone's report will arrive.sendScheduleConfirmationEmail— fires fromcreateTaskHandlerafter the schedule is materialized (the same rule the welcome and valuation emails follow), deduped per task id via a"task_id":"…"marker inemail_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.tsgained 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 howcreateAccountHandler.test.tsmockssendWelcomeEmail.```
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 --noEmitclean for the touched files (repo baseline of 236 pre-existing errors inlib/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
/tasks.email_send_log; skips accounts without an email; never blocks task creation.renderEmailLayoutused by welcome and valuation builders, and by weekly reports viaprocessAndSendEmail.Bug Fixes
styleattributes in email clients.Written for commit a2e00d5. Summary will update on new commits.