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>
118 lines
3.0 KiB
TypeScript
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,
|
|
});
|
|
}
|