Files
HealthLog/src/lib/cycle/cycle-attribution.ts
T
Marc-André BombeckandClaude Opus 4.8 5f485ca780 feat(cycle): add cycle-tracking API surface + offline sync
Add the `/api/cycle/*` capture, calendar, history, and settings routes
plus the offline-sync delta extension for the v1.15 cycle-tracking
feature, all against the locked iOS contract.

- Enable flag: add nullable `cycleTrackingEnabled` to CycleProfile
  (migration 0132). A null toggle derives from gender (FEMALE → on).
  `isCycleEnabled()` is the single gate resolver; every `/api/cycle/*`
  route 403s `{ errorCode: "cycle.disabled" }` when off. The resolved
  flag surfaces additively on GET /api/auth/me.
- Routes (apiHandler + Zod + envelope + gate + annotate + auditLog):
  POST/PATCH/DELETE day-logs (single), POST day-logs/bulk (idempotent,
  capped 500, rate-limited 60/min, per-entry inserted/updated/duplicate/
  skipped), POST period boundary, GET calendar (runs the deterministic
  engine, debounced stale-while-revalidate cache, goal-gated fertile
  window), GET cycles (history + median/MAD stats), GET profile,
  DELETE cycles, PATCH /api/auth/me/cycle-prefs (deep-merge).
- Day-log note encrypts at rest (notesEncrypted); intent fields stay
  queryable plaintext. Upsert keys on (userId, source, externalId) when
  externalId present, else (userId, date).
- Offline sync: extend GET /api/sync/changes + the cursor with the
  `cycleDays` + `cycles` domains (CURSOR_VERSION → 2). cycleDays
  tombstones key on externalId else id; cycles on id.
- OpenAPI: register every route + DTO; regenerate the committed spec.
- i18n: cycle prediction disclaimer across all six locales.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 08:57:12 +02:00

25 lines
677 B
TypeScript

/**
* Attribute a day-log date to its owning `MenstrualCycle` span.
*
* A day-log belongs to the cycle whose `startDate <= date` and which is
* the latest such start (the cycle is open-ended until the next cycle's
* start). Returns the cycle id or null when no cycle precedes the date.
*/
import { prisma } from "@/lib/db";
export async function findOwningCycleId(
userId: string,
date: string,
): Promise<string | null> {
const cycle = await prisma.menstrualCycle.findFirst({
where: {
userId,
deletedAt: null,
startDate: { lte: date },
},
orderBy: { startDate: "desc" },
select: { id: true },
});
return cycle?.id ?? null;
}