// ABOUTME: API route for logging period start dates. // ABOUTME: Recalculates all phase dates when period is logged. import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import { withAuth } from "@/lib/auth-middleware"; import { getCycleDay, getPhase } from "@/lib/cycle"; import { logger } from "@/lib/logger"; import { createPocketBaseClient } from "@/lib/pocketbase"; interface PeriodLogRequest { startDate?: string; } /** * Validates a date string is in YYYY-MM-DD format and represents a valid date. */ function isValidDateFormat(dateStr: string): boolean { const regex = /^\d{4}-\d{2}-\d{2}$/; if (!regex.test(dateStr)) { return false; } const date = new Date(dateStr); return !Number.isNaN(date.getTime()); } /** * Checks if a date is in the future (after today). */ function isFutureDate(dateStr: string): boolean { const inputDate = new Date(dateStr); const today = new Date(); today.setHours(0, 0, 0, 0); inputDate.setHours(0, 0, 0, 0); return inputDate > today; } export const POST = withAuth(async (request: NextRequest, user) => { try { const body = (await request.json()) as PeriodLogRequest; // Validate startDate is present if (!body.startDate) { return NextResponse.json( { error: "startDate is required" }, { status: 400 }, ); } // Validate date format if (!isValidDateFormat(body.startDate)) { return NextResponse.json( { error: "Invalid date format. Use YYYY-MM-DD" }, { status: 400 }, ); } // Validate date is not in the future if (isFutureDate(body.startDate)) { return NextResponse.json( { error: "startDate cannot be in the future" }, { status: 400 }, ); } const pb = createPocketBaseClient(); // Calculate predicted date based on previous cycle (if exists) let predictedDateStr: string | null = null; if (user.lastPeriodDate) { const previousPeriod = new Date(user.lastPeriodDate); const predictedDate = new Date(previousPeriod); predictedDate.setDate(previousPeriod.getDate() + user.cycleLength); predictedDateStr = predictedDate.toISOString().split("T")[0]; } // Update user's lastPeriodDate await pb.collection("users").update(user.id, { lastPeriodDate: body.startDate, }); // Create PeriodLog record with prediction data await pb.collection("period_logs").create({ user: user.id, startDate: body.startDate, predictedDate: predictedDateStr, }); // Calculate updated cycle information const lastPeriodDate = new Date(body.startDate); const cycleDay = getCycleDay(lastPeriodDate, user.cycleLength, new Date()); const phase = getPhase(cycleDay, user.cycleLength); // Calculate prediction accuracy let daysEarly: number | undefined; let daysLate: number | undefined; if (predictedDateStr) { const actual = new Date(body.startDate); const predicted = new Date(predictedDateStr); const diffDays = Math.floor( (predicted.getTime() - actual.getTime()) / (1000 * 60 * 60 * 24), ); if (diffDays > 0) { daysEarly = diffDays; } else if (diffDays < 0) { daysLate = Math.abs(diffDays); } } // Log successful period logging per observability spec logger.info({ userId: user.id, date: body.startDate }, "Period logged"); return NextResponse.json({ message: "Period start date logged successfully", lastPeriodDate: body.startDate, cycleDay, phase, ...(predictedDateStr && { predictedDate: predictedDateStr }), ...(daysEarly !== undefined && { daysEarly }), ...(daysLate !== undefined && { daysLate }), }); } catch (error) { logger.error({ err: error, userId: user.id }, "Period logging error"); return NextResponse.json( { error: "Failed to update period date" }, { status: 500 }, ); } });