mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
feat(meds): plumb v1.5 scheduling fields through POST + PUT routes
The Zod validators + the engine + the worker accept the new v1.5 fields. The API route handlers didn't — they silently dropped `startsOn` / `endsOn` / `oneShot` / `timesOfDay` / `rrule` / `rollingIntervalDays` / `reminderGraceMinutes` before the Prisma write. This commit threads them through. ### Route invariants - One-shot consistency: `oneShot === true` ⇒ at most one schedule (422 otherwise), that schedule has neither `rrule` nor `rollingIntervalDays`, and `endsOn` is auto-normalised to equal `startsOn` on write. - Recurring default: when `oneShot !== true` and the incoming schedule has no `rrule` / `rollingIntervalDays` / legacy `daysOfWeek`, the route stamps `rrule = "FREQ=DAILY"` so the worker always sees a clean shape. - Backwards compat: empty / absent `timesOfDay` is server-side populated from `windowStart` so the engine always sees a non-empty array, and legacy iOS v0.6.x clients that still post the `windowStart` / `windowEnd` / `daysOfWeek` shape keep working unchanged. ### Tests 9 new cases across both route test files (7 POST + 3 PUT, one POST as a 7b subcase): one-shot create persistence, RRULE persistence, rollingIntervalDays persistence, one-shot + recurrence → 422, one-shot + multiple schedules → 422, PUT endsOn updates, legacy POST back-fills timesOfDay, legacy POST with no recurrence hints stamps daily. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
2aa19fa770
commit
925138eccb
4 files changed
+440
-22
No files matched your search
@@ -10,6 +10,12 @@ vi.mock("@/lib/db", () => ({
|
||||
findUnique: vi.fn(),
|
||||
update: vi.fn(),
|
||||
},
|
||||
medicationSchedule: {
|
||||
deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
|
||||
},
|
||||
apiToken: {
|
||||
updateMany: vi.fn().mockResolvedValue({ count: 0 }),
|
||||
},
|
||||
$transaction: vi.fn(),
|
||||
},
|
||||
}));
|
||||
@@ -42,6 +48,8 @@ vi.mock("next/headers", () => ({
|
||||
import { PUT } from "../route";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { getSession } from "@/lib/auth/session";
|
||||
import { getMedicationCategories } from "@/lib/medication-category";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
|
||||
const SESSION_OK = {
|
||||
session: { id: "sess-1", expiresAt: new Date(Date.now() + 3_600_000) },
|
||||
@@ -100,3 +108,68 @@ describe("PUT /api/medications/[id] — 422 multi-issue (v1.4.43 W6)", () => {
|
||||
expect(body.details.issues.length).toBeGreaterThanOrEqual(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PUT /api/medications/[id] — v1.5 scheduling primitives", () => {
|
||||
function lastUpdateCall(): {
|
||||
data: Record<string, unknown>;
|
||||
} {
|
||||
const calls = vi.mocked(prisma.medication.update).mock.calls;
|
||||
expect(calls.length).toBeGreaterThan(0);
|
||||
return calls[calls.length - 1][0] as never;
|
||||
}
|
||||
|
||||
it("(6) PATCH endsOn updates the field", async () => {
|
||||
vi.mocked(prisma.medication.update).mockResolvedValue({
|
||||
id: "m1",
|
||||
userId: "user-1",
|
||||
endsOn: new Date("2026-12-31"),
|
||||
schedules: [],
|
||||
} as never);
|
||||
// Re-stub deps the outer beforeEach's resetAllMocks cleared.
|
||||
vi.mocked(getMedicationCategories).mockResolvedValue({});
|
||||
vi.mocked(auditLog).mockResolvedValue(undefined);
|
||||
const res = await PUT(
|
||||
putReq({ endsOn: "2026-12-31" }),
|
||||
ROUTE_CTX,
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
const call = lastUpdateCall();
|
||||
expect(call.data.endsOn).toBeInstanceOf(Date);
|
||||
expect((call.data.endsOn as Date).toISOString()).toBe(
|
||||
new Date("2026-12-31").toISOString(),
|
||||
);
|
||||
});
|
||||
|
||||
it("(4-PUT) 422 when oneShot=true + schedule carries rrule", async () => {
|
||||
const res = await PUT(
|
||||
putReq({
|
||||
oneShot: true,
|
||||
startsOn: "2026-10-15",
|
||||
schedules: [
|
||||
{
|
||||
windowStart: "10:00",
|
||||
windowEnd: "10:30",
|
||||
rrule: "FREQ=DAILY",
|
||||
},
|
||||
],
|
||||
}),
|
||||
ROUTE_CTX,
|
||||
);
|
||||
expect(res.status).toBe(422);
|
||||
});
|
||||
|
||||
it("(5-PUT) 422 when oneShot=true + multiple schedules", async () => {
|
||||
const res = await PUT(
|
||||
putReq({
|
||||
oneShot: true,
|
||||
startsOn: "2026-10-15",
|
||||
schedules: [
|
||||
{ windowStart: "08:00", windowEnd: "08:30" },
|
||||
{ windowStart: "20:00", windowEnd: "20:30" },
|
||||
],
|
||||
}),
|
||||
ROUTE_CTX,
|
||||
);
|
||||
expect(res.status).toBe(422);
|
||||
});
|
||||
});
|
||||
@@ -83,8 +83,42 @@ export const PUT = apiHandler(
|
||||
active,
|
||||
notificationsEnabled,
|
||||
schedules,
|
||||
startsOn,
|
||||
endsOn,
|
||||
oneShot,
|
||||
} = parsed.data;
|
||||
|
||||
// ── v1.5 route invariants for the new scheduling primitives ─────
|
||||
//
|
||||
// Mirrors the POST route: oneShot ⇒ at-most-one schedule + no
|
||||
// recurrence on it; endsOn normalised to startsOn for one-shot;
|
||||
// recurring default = FREQ=DAILY when nothing else is set;
|
||||
// timesOfDay dual-write from windowStart when absent.
|
||||
//
|
||||
// The PUT replaces all schedules wholesale, so the invariants run
|
||||
// against the incoming `schedules` array (if provided) the same
|
||||
// way they do on create.
|
||||
if (oneShot === true && schedules) {
|
||||
if (schedules.length > 1) {
|
||||
return apiError(
|
||||
"A one-shot medication can have at most one schedule",
|
||||
422,
|
||||
);
|
||||
}
|
||||
const s = schedules[0];
|
||||
if (s && (s.rrule !== undefined || s.rollingIntervalDays !== undefined)) {
|
||||
return apiError(
|
||||
"A one-shot medication cannot have a recurrence (rrule or rollingIntervalDays)",
|
||||
422,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Normalise endsOn for one-shot. `oneShot === true` + `startsOn`
|
||||
// means the dose is the start date; endsOn auto-matches.
|
||||
const normalisedEndsOn =
|
||||
oneShot === true && startsOn ? startsOn : endsOn;
|
||||
|
||||
const pausedAtPatch =
|
||||
active === undefined
|
||||
? {}
|
||||
@@ -108,18 +142,51 @@ export const PUT = apiHandler(
|
||||
...(dosesPerUnit !== undefined && { dosesPerUnit }),
|
||||
...(active !== undefined && { active }),
|
||||
...(notificationsEnabled !== undefined && { notificationsEnabled }),
|
||||
// v1.5 scheduling primitives — pass-through when supplied.
|
||||
// `startsOn` / `endsOn` are `Date | null | undefined` (the
|
||||
// schema lets the user clear them explicitly with null).
|
||||
...(startsOn !== undefined && { startsOn }),
|
||||
...(normalisedEndsOn !== undefined && { endsOn: normalisedEndsOn }),
|
||||
...(oneShot !== undefined && { oneShot }),
|
||||
...(schedules && {
|
||||
schedules: {
|
||||
create: schedules.map((s) => ({
|
||||
windowStart: s.windowStart,
|
||||
windowEnd: s.windowEnd,
|
||||
label: s.label ?? null,
|
||||
dose: s.dose ?? null,
|
||||
daysOfWeek: serializeScheduleRecurrence({
|
||||
daysOfWeek: s.daysOfWeek ?? [],
|
||||
intervalWeeks: s.intervalWeeks ?? 1,
|
||||
}),
|
||||
})),
|
||||
create: schedules.map((s) => {
|
||||
// Invariant 2 — default to FREQ=DAILY when nothing else is set.
|
||||
const hasLegacyDays = (s.daysOfWeek?.length ?? 0) > 0;
|
||||
const defaultedRrule =
|
||||
oneShot !== true &&
|
||||
s.rrule === undefined &&
|
||||
s.rollingIntervalDays === undefined &&
|
||||
!hasLegacyDays
|
||||
? "FREQ=DAILY"
|
||||
: s.rrule;
|
||||
|
||||
// Invariant 3 — dual-write timesOfDay from windowStart.
|
||||
const effectiveTimesOfDay =
|
||||
s.timesOfDay && s.timesOfDay.length > 0
|
||||
? s.timesOfDay
|
||||
: [s.windowStart];
|
||||
|
||||
return {
|
||||
windowStart: s.windowStart,
|
||||
windowEnd: s.windowEnd,
|
||||
label: s.label ?? null,
|
||||
dose: s.dose ?? null,
|
||||
daysOfWeek: serializeScheduleRecurrence({
|
||||
daysOfWeek: s.daysOfWeek ?? [],
|
||||
intervalWeeks: s.intervalWeeks ?? 1,
|
||||
}),
|
||||
// v1.5 first-class times-of-day.
|
||||
timesOfDay: effectiveTimesOfDay,
|
||||
...(s.reminderGraceMinutes !== undefined && {
|
||||
reminderGraceMinutes: s.reminderGraceMinutes,
|
||||
}),
|
||||
...(defaultedRrule !== undefined && { rrule: defaultedRrule }),
|
||||
...(s.rollingIntervalDays !== undefined && {
|
||||
rollingIntervalDays: s.rollingIntervalDays,
|
||||
}),
|
||||
};
|
||||
}),
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -13,6 +13,9 @@ vi.mock("@/lib/db", () => ({
|
||||
medicationCategoryAssignment: {
|
||||
findMany: vi.fn().mockResolvedValue([]),
|
||||
},
|
||||
medicationSchedule: {
|
||||
deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
|
||||
},
|
||||
$transaction: vi.fn(),
|
||||
},
|
||||
}));
|
||||
@@ -54,6 +57,7 @@ vi.mock("next/headers", () => ({
|
||||
}));
|
||||
|
||||
import { POST } from "../route";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { getSession } from "@/lib/auth/session";
|
||||
|
||||
const SESSION_OK = {
|
||||
@@ -72,6 +76,31 @@ function postReq(body: unknown): NextRequest {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
|
||||
// Default happy-path stub: return the data shape the caller passed in
|
||||
// so test assertions can inspect what the route actually forwarded
|
||||
// to Prisma without re-implementing the create-returning DB.
|
||||
vi.mocked(prisma.medication.create).mockImplementation((async (
|
||||
args: { data: Record<string, unknown> },
|
||||
) => ({
|
||||
id: "med-1",
|
||||
userId: "user-1",
|
||||
...args.data,
|
||||
// Reflect the nested `schedules.create` as a populated array so
|
||||
// the include returns the same shape as Prisma.
|
||||
schedules: Array.isArray(
|
||||
(args.data as { schedules?: { create?: unknown[] } }).schedules?.create,
|
||||
)
|
||||
? (
|
||||
args.data as {
|
||||
schedules: { create: Record<string, unknown>[] };
|
||||
}
|
||||
).schedules.create.map((s, i) => ({
|
||||
id: `sched-${i}`,
|
||||
medicationId: "med-1",
|
||||
...s,
|
||||
}))
|
||||
: [],
|
||||
})) as never);
|
||||
});
|
||||
|
||||
describe("POST /api/medications — 422 multi-issue (v1.4.43 W6)", () => {
|
||||
@@ -106,3 +135,166 @@ describe("POST /api/medications — 422 multi-issue (v1.4.43 W6)", () => {
|
||||
expect(body.details.issues.length).toBeGreaterThanOrEqual(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/medications — v1.5 scheduling primitives", () => {
|
||||
/**
|
||||
* Helper to extract the `data.schedules.create[i]` payload the route
|
||||
* forwarded to Prisma.create — that's the single point of truth this
|
||||
* test suite cares about.
|
||||
*/
|
||||
function lastCreateCall(): {
|
||||
data: Record<string, unknown> & {
|
||||
schedules: { create: Record<string, unknown>[] };
|
||||
};
|
||||
} {
|
||||
const calls = vi.mocked(prisma.medication.create).mock.calls;
|
||||
expect(calls.length).toBeGreaterThan(0);
|
||||
return calls[calls.length - 1][0] as never;
|
||||
}
|
||||
|
||||
it("(1) one-shot create writes oneShot=true + single timesOfDay schedule", async () => {
|
||||
const res = await POST(
|
||||
postReq({
|
||||
name: "Flu shot",
|
||||
dose: "1 shot",
|
||||
oneShot: true,
|
||||
startsOn: "2026-10-15",
|
||||
schedules: [
|
||||
{ windowStart: "10:00", windowEnd: "10:30", timesOfDay: ["10:00"] },
|
||||
],
|
||||
}),
|
||||
);
|
||||
expect(res.status).toBe(201);
|
||||
const call = lastCreateCall();
|
||||
expect(call.data.oneShot).toBe(true);
|
||||
// endsOn auto-normalised to startsOn for one-shot.
|
||||
expect(call.data.endsOn).toBeInstanceOf(Date);
|
||||
expect((call.data.endsOn as Date).toISOString()).toEqual(
|
||||
(call.data.startsOn as Date).toISOString(),
|
||||
);
|
||||
const s = call.data.schedules.create[0];
|
||||
expect(s.timesOfDay).toEqual(["10:00"]);
|
||||
expect(s.rrule).toBeUndefined();
|
||||
expect(s.rollingIntervalDays).toBeUndefined();
|
||||
});
|
||||
|
||||
it("(2) RRULE schedule is persisted verbatim", async () => {
|
||||
const res = await POST(
|
||||
postReq({
|
||||
name: "Monthly med",
|
||||
dose: "1 tab",
|
||||
schedules: [
|
||||
{
|
||||
windowStart: "08:00",
|
||||
windowEnd: "08:30",
|
||||
rrule: "FREQ=MONTHLY;BYMONTHDAY=1",
|
||||
timesOfDay: ["08:00"],
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
expect(res.status).toBe(201);
|
||||
const s = lastCreateCall().data.schedules.create[0];
|
||||
expect(s.rrule).toBe("FREQ=MONTHLY;BYMONTHDAY=1");
|
||||
expect(s.timesOfDay).toEqual(["08:00"]);
|
||||
});
|
||||
|
||||
it("(3) rolling schedule is persisted with rollingIntervalDays", async () => {
|
||||
const res = await POST(
|
||||
postReq({
|
||||
name: "Mounjaro",
|
||||
dose: "5mg",
|
||||
schedules: [
|
||||
{
|
||||
windowStart: "09:00",
|
||||
windowEnd: "09:30",
|
||||
rollingIntervalDays: 7,
|
||||
timesOfDay: ["09:00"],
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
expect(res.status).toBe(201);
|
||||
const s = lastCreateCall().data.schedules.create[0];
|
||||
expect(s.rollingIntervalDays).toBe(7);
|
||||
expect(s.rrule).toBeUndefined();
|
||||
expect(s.timesOfDay).toEqual(["09:00"]);
|
||||
});
|
||||
|
||||
it("(4) 422 when oneShot=true AND schedule carries recurrence", async () => {
|
||||
const res = await POST(
|
||||
postReq({
|
||||
name: "Bad one-shot",
|
||||
dose: "1",
|
||||
oneShot: true,
|
||||
startsOn: "2026-10-15",
|
||||
schedules: [
|
||||
{
|
||||
windowStart: "10:00",
|
||||
windowEnd: "10:30",
|
||||
rrule: "FREQ=DAILY",
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
expect(res.status).toBe(422);
|
||||
const body = (await res.json()) as { error: string };
|
||||
expect(body.error).toMatch(/one-shot.*recurrence/i);
|
||||
});
|
||||
|
||||
it("(5) 422 when oneShot=true AND multiple schedules", async () => {
|
||||
const res = await POST(
|
||||
postReq({
|
||||
name: "Bad one-shot",
|
||||
dose: "1",
|
||||
oneShot: true,
|
||||
startsOn: "2026-10-15",
|
||||
schedules: [
|
||||
{ windowStart: "08:00", windowEnd: "08:30" },
|
||||
{ windowStart: "20:00", windowEnd: "20:30" },
|
||||
],
|
||||
}),
|
||||
);
|
||||
expect(res.status).toBe(422);
|
||||
const body = (await res.json()) as { error: string };
|
||||
expect(body.error).toMatch(/one-shot.*one schedule/i);
|
||||
});
|
||||
|
||||
it("(7) legacy POST without new fields back-fills timesOfDay from windowStart", async () => {
|
||||
const res = await POST(
|
||||
postReq({
|
||||
name: "Daily vitamin",
|
||||
dose: "1 tab",
|
||||
schedules: [
|
||||
{
|
||||
windowStart: "08:00",
|
||||
windowEnd: "09:00",
|
||||
daysOfWeek: [1, 2, 3, 4, 5],
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
expect(res.status).toBe(201);
|
||||
const s = lastCreateCall().data.schedules.create[0];
|
||||
// timesOfDay populated server-side from windowStart.
|
||||
expect(s.timesOfDay).toEqual(["08:00"]);
|
||||
// Legacy daysOfWeek serialised; new rrule not stamped because
|
||||
// legacy days were supplied.
|
||||
expect(s.daysOfWeek).toBe("1,2,3,4,5");
|
||||
expect(s.rrule).toBeUndefined();
|
||||
});
|
||||
|
||||
it("(7b) legacy POST with no daysOfWeek, no rrule, no rolling → defaults rrule=FREQ=DAILY", async () => {
|
||||
const res = await POST(
|
||||
postReq({
|
||||
name: "Plain daily",
|
||||
dose: "1 tab",
|
||||
schedules: [{ windowStart: "08:00", windowEnd: "09:00" }],
|
||||
}),
|
||||
);
|
||||
expect(res.status).toBe(201);
|
||||
const s = lastCreateCall().data.schedules.create[0];
|
||||
expect(s.rrule).toBe("FREQ=DAILY");
|
||||
expect(s.timesOfDay).toEqual(["08:00"]);
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import { annotate, getEvent } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import {
|
||||
apiError,
|
||||
apiSuccess,
|
||||
getClientIp,
|
||||
returnAllZodIssues,
|
||||
@@ -114,8 +115,61 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
|
||||
const { name, dose, category, treatmentClass, dosesPerUnit, schedules } =
|
||||
parsed.data;
|
||||
const {
|
||||
name,
|
||||
dose,
|
||||
category,
|
||||
treatmentClass,
|
||||
dosesPerUnit,
|
||||
schedules,
|
||||
startsOn,
|
||||
endsOn,
|
||||
oneShot,
|
||||
} = parsed.data;
|
||||
|
||||
// ── v1.5 route invariants for the new scheduling primitives ───────
|
||||
//
|
||||
// The Zod layer already enforces `rrule` xor `rollingIntervalDays`
|
||||
// (scheduleSchema.refine). The route layer enforces the cross-field
|
||||
// invariants that span medication + schedules:
|
||||
//
|
||||
// 1. **One-shot consistency** — when `oneShot === true`:
|
||||
// a. The medication has at most ONE schedule.
|
||||
// b. The single schedule carries neither `rrule` nor
|
||||
// `rollingIntervalDays` (a one-shot dose IS the schedule).
|
||||
// c. `endsOn` is normalised to equal `startsOn` on write so the
|
||||
// worker's date-range cap matches the one-and-only slot.
|
||||
//
|
||||
// 2. **Recurring default** — when `oneShot !== true` AND the
|
||||
// schedule has no `rrule`, no `rollingIntervalDays`, and no
|
||||
// legacy `daysOfWeek`, the route stamps `rrule = "FREQ=DAILY"`
|
||||
// so the canonical recurrence engine has a clean shape going
|
||||
// forward instead of falling through to the legacy parser.
|
||||
//
|
||||
// 3. **iOS contract dual-write** — when `timesOfDay` is absent or
|
||||
// empty, the route stamps `[windowStart]` so the new engine
|
||||
// always sees a populated `timesOfDay` while legacy iOS clients
|
||||
// (v0.6.x) keep encoding the `windowStart` / `windowEnd` /
|
||||
// `daysOfWeek` shape. Both shapes coexist through v1.5.x.
|
||||
if (oneShot === true) {
|
||||
if (schedules.length > 1) {
|
||||
return apiError(
|
||||
"A one-shot medication can have at most one schedule",
|
||||
422,
|
||||
);
|
||||
}
|
||||
const s = schedules[0];
|
||||
if (s.rrule !== undefined || s.rollingIntervalDays !== undefined) {
|
||||
return apiError(
|
||||
"A one-shot medication cannot have a recurrence (rrule or rollingIntervalDays)",
|
||||
422,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Normalise endsOn for one-shot. Zod gives us `Date | null | undefined`.
|
||||
const normalisedEndsOn =
|
||||
oneShot === true && startsOn ? startsOn : (endsOn ?? undefined);
|
||||
|
||||
const medication = await prisma.medication.create({
|
||||
data: {
|
||||
@@ -126,17 +180,49 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
// wire schema; Prisma fills the default GENERIC when omitted.
|
||||
...(treatmentClass !== undefined && { treatmentClass }),
|
||||
...(dosesPerUnit !== undefined && { dosesPerUnit }),
|
||||
// v1.5 scheduling primitives — pass-through when supplied.
|
||||
...(startsOn !== undefined && { startsOn }),
|
||||
...(normalisedEndsOn !== undefined && { endsOn: normalisedEndsOn }),
|
||||
...(oneShot !== undefined && { oneShot }),
|
||||
schedules: {
|
||||
create: schedules.map((s) => ({
|
||||
windowStart: s.windowStart,
|
||||
windowEnd: s.windowEnd,
|
||||
label: s.label ?? null,
|
||||
dose: s.dose ?? null,
|
||||
daysOfWeek: serializeScheduleRecurrence({
|
||||
daysOfWeek: s.daysOfWeek ?? [],
|
||||
intervalWeeks: s.intervalWeeks ?? 1,
|
||||
}),
|
||||
})),
|
||||
create: schedules.map((s) => {
|
||||
// Invariant 2 — default to FREQ=DAILY when nothing else is set.
|
||||
const hasLegacyDays = (s.daysOfWeek?.length ?? 0) > 0;
|
||||
const defaultedRrule =
|
||||
!oneShot &&
|
||||
s.rrule === undefined &&
|
||||
s.rollingIntervalDays === undefined &&
|
||||
!hasLegacyDays
|
||||
? "FREQ=DAILY"
|
||||
: s.rrule;
|
||||
|
||||
// Invariant 3 — dual-write `timesOfDay` from windowStart when
|
||||
// the legacy-shape iOS client doesn't send the new field.
|
||||
const effectiveTimesOfDay =
|
||||
s.timesOfDay && s.timesOfDay.length > 0
|
||||
? s.timesOfDay
|
||||
: [s.windowStart];
|
||||
|
||||
return {
|
||||
windowStart: s.windowStart,
|
||||
windowEnd: s.windowEnd,
|
||||
label: s.label ?? null,
|
||||
dose: s.dose ?? null,
|
||||
daysOfWeek: serializeScheduleRecurrence({
|
||||
daysOfWeek: s.daysOfWeek ?? [],
|
||||
intervalWeeks: s.intervalWeeks ?? 1,
|
||||
}),
|
||||
// v1.5 first-class times-of-day.
|
||||
timesOfDay: effectiveTimesOfDay,
|
||||
...(s.reminderGraceMinutes !== undefined && {
|
||||
reminderGraceMinutes: s.reminderGraceMinutes,
|
||||
}),
|
||||
...(defaultedRrule !== undefined && { rrule: defaultedRrule }),
|
||||
...(s.rollingIntervalDays !== undefined && {
|
||||
rollingIntervalDays: s.rollingIntervalDays,
|
||||
}),
|
||||
};
|
||||
}),
|
||||
},
|
||||
},
|
||||
include: { schedules: true },
|
||||
|
||||
Reference in new issue
Block a user