Overview
This release introduces a complete transactional email system built on React Email templates and the existing @kit/mailers abstraction (Resend or Nodemailer). Six new email templates cover the full quote lifecycle, account onboarding, and weekly activity digests — all with i18n support and a single environment toggle to enable or disable delivery.
New Email Templates
All templates live in packages/email-templates/src/emails/ and follow the same pattern as existing templates (invite, OTP, account-delete): shared components, Tailwind styling, and initializeEmailI18n for translation.
| Template | Trigger | Recipients |
|---|---|---|
| Quote Submitted | After submitQuoteAction inserts a new quote | Customer + assigned sales rep |
| Quote Status Update | After updateQuoteStatusAction or cancelQuoteAction | Customer (contact_email) |
| Email Your Rep | sendRepEmailAction (replaces inline HTML) | Assigned sales rep |
| Weekly Recap | Vercel Cron — every Monday at 8 AM UTC | All active accounts |
| Welcome | Database webhook on accounts INSERT | New user |
| Quote Expiring | Vercel Cron — daily at 9 AM UTC | Customers with quotes expiring within 3 days |
Quote Submitted
Sends a branded confirmation with the quote number, sport, product name, roster count, uniform preview thumbnail, and a CTA to view the quote. When a sales rep is assigned to the account, a copy is also sent to the rep.
Quote Status Update
Triggered on any workflow status transition (quoted, approved, rejected, expired, ordered, cancelled). Includes a color-coded status badge and status-specific messaging — e.g., "Great news! Your quote has been priced" for quoted.
Email Your Rep (Upgrade)
Previously used raw inline HTML. Now renders a proper React Email template with consistent ASB Sports branding, sender info block, and message body. The replyTo header now works correctly with both Resend and Nodemailer (see bug fix below).
Weekly Recap
A digest email showing the past week's activity: quotes created, quotes priced, quotes approved, orders placed, pending quotes, and total quotes. Includes the assigned sales rep's contact info and a CTA to the dashboard.
Welcome
Sent when a new personal account is created (via database webhook). Introduces ASB Sports with a 3-step getting-started guide: design your uniform, get a quote, place your order.
Quote Expiring
A reminder sent to customers whose quotes are expiring within 3 days. Shows the quote number, expiration date in a warning-styled card, and a CTA to review and approve.
QuoteEmailService
New centralized service at apps/web/lib/server/emails/quote-email.service.ts with static methods:
| Method | Purpose |
|---|---|
sendQuoteSubmitted() | Customer + rep notification on new quote |
sendStatusUpdate() | Customer notification on status change |
sendQuoteExpiring() | Expiry reminder |
sendWeeklyRecap() | Weekly digest for one account |
sendWelcome() | Onboarding email for new accounts |
All methods are fire-and-forget — wrapped in try/catch with structured logging so email failures never block the primary operation.
Vercel Cron Infrastructure
New apps/web/vercel.json with two cron schedules:
| Route | Schedule | Purpose |
|---|---|---|
/api/cron/weekly-recap | 0 8 * * 1 (Monday 8 AM UTC) | Batch weekly recap emails |
/api/cron/quote-expiry-reminder | 0 9 * * * (Daily 9 AM UTC) | Quote expiry reminders |
Both routes are protected by CRON_SECRET bearer token authentication.
Bug Fix — Resend replyTo Support
The MailerSchema in @kit/mailers-shared now includes an optional replyTo field. The Resend HTTP mailer forwards it as reply_to in the API payload. Previously, replyTo set by sendRepEmailAction was silently dropped when using Resend.
Environment Toggle
New environment variable ENABLE_TRANSACTIONAL_EMAILS controls whether transactional emails are sent:
| Environment | Default | Behavior |
|---|---|---|
Local (.env) | false | All transactional emails silently skipped |
Development (.env.development) | true | Emails active |
Production (.env.production) | true | Emails active |
When disabled, the QuoteEmailService.guard() method logs a skip message with full context and returns early — no template rendering, no mailer calls.
Database Webhook Extension
The existing /api/db/webhook route now includes an app-level handleEvent callback that listens for accounts INSERT events and dispatches the welcome email for new personal accounts.
i18n Locale Files
Six new translation files in packages/email-templates/src/locales/en/:
quote-submitted-email.jsonquote-status-update-email.jsonemail-rep-email.jsonweekly-recap-email.jsonwelcome-email.jsonquote-expiring-email.json