Transactional Email System

Six branded React Email templates, centralized QuoteEmailService, Vercel Cron for weekly recap and quote expiry reminders, replyTo bug fix, and ENABLE_TRANSACTIONAL_EMAILS kill switch.

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.

TemplateTriggerRecipients
Quote SubmittedAfter submitQuoteAction inserts a new quoteCustomer + assigned sales rep
Quote Status UpdateAfter updateQuoteStatusAction or cancelQuoteActionCustomer (contact_email)
Email Your RepsendRepEmailAction (replaces inline HTML)Assigned sales rep
Weekly RecapVercel Cron — every Monday at 8 AM UTCAll active accounts
WelcomeDatabase webhook on accounts INSERTNew user
Quote ExpiringVercel Cron — daily at 9 AM UTCCustomers 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:

MethodPurpose
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:

RouteSchedulePurpose
/api/cron/weekly-recap0 8 * * 1 (Monday 8 AM UTC)Batch weekly recap emails
/api/cron/quote-expiry-reminder0 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:

EnvironmentDefaultBehavior
Local (.env)falseAll transactional emails silently skipped
Development (.env.development)trueEmails active
Production (.env.production)trueEmails 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.json
  • quote-status-update-email.json
  • email-rep-email.json
  • weekly-recap-email.json
  • welcome-email.json
  • quote-expiring-email.json