Files
phaseflow/src/lib/email.ts
Petru Paler 2ffee63a59 Implement token expiration warnings (P3.9)
Add email warnings for Garmin token expiration at 14-day and 7-day thresholds.
When the garmin-sync cron job runs, it now checks each user's token expiry and
sends a warning email at exactly 14 days and 7 days before expiration.

Changes:
- Add sendTokenExpirationWarning() to email.ts with differentiated subject
  lines and urgency levels for 14-day vs 7-day warnings
- Integrate warning logic into garmin-sync cron route using daysUntilExpiry()
- Track warnings sent in sync response with new warningsSent counter
- Add 20 new tests (10 for email function, 10 for sync integration)

Test count: 517 → 537

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 08:24:19 +00:00

118 lines
3.0 KiB
TypeScript

// ABOUTME: Email sending utilities using Resend.
// ABOUTME: Sends daily training notifications and period confirmation emails.
import { Resend } from "resend";
const resend = new Resend(process.env.RESEND_API_KEY);
const EMAIL_FROM = process.env.EMAIL_FROM || "phaseflow@example.com";
export interface DailyEmailData {
to: string;
cycleDay: number;
phase: string;
decision: {
status: string;
reason: string;
icon: string;
};
bodyBatteryCurrent: number | null;
bodyBatteryYesterdayLow: number | null;
hrvStatus: string;
weekIntensity: number;
phaseLimit: number;
remainingMinutes: number;
seeds: string;
carbRange: string;
ketoGuidance: string;
}
export async function sendDailyEmail(data: DailyEmailData): Promise<void> {
const subject = `Today's Training: ${data.decision.icon} ${data.decision.status}`;
const body = `Good morning!
📅 CYCLE DAY: ${data.cycleDay} (${data.phase})
💪 TODAY'S PLAN:
${data.decision.icon} ${data.decision.reason}
📊 YOUR DATA:
• Body Battery Now: ${data.bodyBatteryCurrent ?? "N/A"}
• Yesterday's Low: ${data.bodyBatteryYesterdayLow ?? "N/A"}
• HRV Status: ${data.hrvStatus}
• Week Intensity: ${data.weekIntensity} / ${data.phaseLimit} minutes
• Remaining: ${data.remainingMinutes} minutes
🌱 SEEDS: ${data.seeds}
🍽️ MACROS: ${data.carbRange}
🥑 KETO: ${data.ketoGuidance}
---
Auto-generated by PhaseFlow`;
await resend.emails.send({
from: EMAIL_FROM,
to: data.to,
subject,
text: body,
});
}
export async function sendPeriodConfirmationEmail(
to: string,
lastPeriodDate: Date,
cycleLength: number,
): Promise<void> {
const subject = "🔵 Period Tracking Updated";
const body = `Your cycle has been reset. Last period: ${lastPeriodDate.toLocaleDateString()}
Phase calendar updated for next ${cycleLength} days.
Your calendar will update automatically within 24 hours.
---
Auto-generated by PhaseFlow`;
await resend.emails.send({
from: EMAIL_FROM,
to,
subject,
text: body,
});
}
export async function sendTokenExpirationWarning(
to: string,
daysUntilExpiry: number,
): Promise<void> {
const isUrgent = daysUntilExpiry <= 7;
const subject = isUrgent
? `🚨 PhaseFlow: Garmin tokens expire in ${daysUntilExpiry} days - action required`
: `⚠️ PhaseFlow: Garmin tokens expire in ${daysUntilExpiry} days`;
const urgencyMessage = isUrgent
? `⚠️ This is urgent - your Garmin data sync will stop working in ${daysUntilExpiry} days if you don't refresh your tokens.`
: `Your Garmin OAuth tokens will expire in ${daysUntilExpiry} days.`;
const body = `${urgencyMessage}
📋 HOW TO REFRESH YOUR TOKENS:
1. Go to Settings > Garmin in PhaseFlow
2. Follow the instructions to reconnect your Garmin account
3. Paste the new tokens from the bootstrap script
This will ensure your training recommendations continue to use fresh Garmin data.
---
Auto-generated by PhaseFlow`;
await resend.emails.send({
from: EMAIL_FROM,
to,
subject,
text: body,
});
}