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>
This commit is contained in:
2026-01-11 08:24:19 +00:00
parent 6c3dd34412
commit 2ffee63a59
5 changed files with 304 additions and 19 deletions

View File

@@ -4,8 +4,10 @@ import { NextResponse } from "next/server";
import { getCycleDay, getPhase, getPhaseLimit } from "@/lib/cycle";
import { getDecisionWithOverrides } from "@/lib/decision-engine";
import { sendTokenExpirationWarning } from "@/lib/email";
import { decrypt } from "@/lib/encryption";
import {
daysUntilExpiry,
fetchBodyBattery,
fetchHrvStatus,
fetchIntensityMinutes,
@@ -19,6 +21,7 @@ interface SyncResult {
usersProcessed: number;
errors: number;
skippedExpired: number;
warningsSent: number;
timestamp: string;
}
@@ -36,6 +39,7 @@ export async function POST(request: Request) {
usersProcessed: 0,
errors: 0,
skippedExpired: 0,
warningsSent: 0,
timestamp: new Date().toISOString(),
};
@@ -61,6 +65,17 @@ export async function POST(request: Request) {
continue;
}
// Check for token expiration warnings (exactly 14 or 7 days)
const daysRemaining = daysUntilExpiry(tokens);
if (daysRemaining === 14 || daysRemaining === 7) {
try {
await sendTokenExpirationWarning(user.email, daysRemaining);
result.warningsSent++;
} catch {
// Continue processing even if warning email fails
}
}
// Decrypt OAuth2 token
const oauth2Json = decrypt(user.garminOauth2Token);
const oauth2Data = JSON.parse(oauth2Json);