Merge branch 'v1424/fixes' into release/v1.42.4

This commit is contained in:
Marc-André Bombeck committed 2026-10-15 12:23:55 +02:00
commit de40db7ad0
49 files changed
+2579 -269

No files matched your search

+6 -2
View File
@@ -63,6 +63,7 @@ export const GET = apiHandler(async (request: NextRequest) => {
practitionerId: params.get("practitionerId") ?? undefined,
episodeId: params.get("episodeId") ?? undefined,
limit: params.get("limit") ?? undefined,
offset: params.get("offset") ?? undefined,
});
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422, {
@@ -72,6 +73,7 @@ export const GET = apiHandler(async (request: NextRequest) => {
const query = parsed.data;
const take = query.limit ?? 100;
const skip = query.offset ?? 0;
const now = new Date();
const where = {
@@ -112,8 +114,9 @@ export const GET = apiHandler(async (request: NextRequest) => {
// `gt` and `gte` are different keys, so the caller's lower bound and this
// arm's floor both survive the spread; only the `lte` pair collided.
where: { ...where, occurredAt: { ...where.occurredAt, gt: now } },
orderBy: { occurredAt: "asc" },
orderBy: [{ occurredAt: "asc" }, { id: "asc" }],
take,
skip,
include: ENCOUNTER_INCLUDE,
}),
prisma.encounter.findMany({
@@ -121,8 +124,9 @@ export const GET = apiHandler(async (request: NextRequest) => {
...where,
occurredAt: { ...where.occurredAt, lte: pastCeiling },
},
orderBy: { occurredAt: "desc" },
orderBy: [{ occurredAt: "desc" }, { id: "desc" }],
take,
skip,
include: ENCOUNTER_INCLUDE,
}),
]);
+19 -1
View File
@@ -43,6 +43,11 @@ export const GET = apiHandler(async (request: NextRequest) => {
const parsed = illnessEpisodeListQuerySchema.safeParse({
limit: params.get("limit") ?? undefined,
includeResolved: params.get("includeResolved") ?? undefined,
offset: params.get("offset") ?? undefined,
from: params.get("from") ?? undefined,
to: params.get("to") ?? undefined,
sortDir: params.get("sortDir") ?? undefined,
q: params.get("q") ?? undefined,
});
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422, {
@@ -50,15 +55,28 @@ export const GET = apiHandler(async (request: NextRequest) => {
});
}
const { from, to, q, offset, sortDir } = parsed.data;
const limit = parsed.data.limit ?? 50;
const order = sortDir ?? "desc";
const rows = await prisma.illnessEpisode.findMany({
where: {
userId: user.id,
deletedAt: null,
...(parsed.data.includeResolved === "false" ? { resolvedAt: null } : {}),
...(from || to
? {
onsetAt: {
...(from ? { gte: new Date(from) } : {}),
...(to ? { lte: new Date(to) } : {}),
},
}
: {}),
...(q ? { label: { contains: q, mode: "insensitive" as const } } : {}),
},
orderBy: { onsetAt: "desc" },
// `id` breaks ties so offset paging never repeats or drops a row.
orderBy: [{ onsetAt: order }, { id: order }],
take: limit,
skip: offset ?? 0,
});
annotate({
+7 -1
View File
@@ -85,7 +85,7 @@ export const GET = apiHandler(async (request: NextRequest) => {
return returnAllZodIssues(parsed.error, 422);
}
const { biomarkerId, analyte, panel, from, to, limit, offset, sortDir } =
const { biomarkerId, analyte, panel, q, from, to, limit, offset, sortDir } =
parsed.data;
const where = {
@@ -94,6 +94,12 @@ export const GET = apiHandler(async (request: NextRequest) => {
...(biomarkerId && { biomarkerId }),
...(analyte && { analyte }),
...(panel && { panel }),
...(q && {
OR: [
{ analyte: { contains: q, mode: "insensitive" as const } },
{ panel: { contains: q, mode: "insensitive" as const } },
],
}),
...(from || to
? {
takenAt: {
@@ -1380,9 +1380,11 @@ describe("POST /api/medications/intake/bulk — v1.28 Apple Health dose events",
});
it("stamps the externalId onto the pending slot row when the Apple dose converges by band", async () => {
// Dedup pre-check first (empty), then the canonical-slot find returns
// the worker-minted pending REMINDER row for the 19:00 slot.
// Dedup pre-check first (empty), then the held-dose probe (no take on
// the slot yet), then the canonical-slot find returns the worker-minted
// pending REMINDER row for the 19:00 slot.
vi.mocked(prisma.medicationIntakeEvent.findMany)
.mockResolvedValueOnce([] as never)
.mockResolvedValueOnce([] as never)
.mockResolvedValueOnce([
{
@@ -65,6 +65,7 @@ import {
resolveForcedSlotForWrite,
resolveSlotForWriteByBand,
resolveSlotInstantForWrite,
slotHeldByAnotherDose,
} from "@/lib/medications/scheduling/slot-upsert";
import { resolveInjectionSiteForWrite } from "@/lib/medications/injection-site-write";
import {
@@ -338,6 +339,9 @@ async function postBulk(request: NextRequest): Promise<Response> {
let inserted = 0;
let updated = 0;
let duplicates = 0;
// External dose records kept as their own unscheduled dose because their
// band slot already carried a different one.
let extraDosesUnscheduled = 0;
const skipped: Array<{ index: number; reason: string }> = [];
const unstableShapes: UnstableExternalIdShape[] = [];
// v1.4.39 W-MED — collect distinct `(medicationId, dayKey)` pairs
@@ -443,6 +447,9 @@ async function postBulk(request: NextRequest): Promise<Response> {
// a band decision. Pending echoes / skips carry no decision
// (undefined → the upsert leaves an existing USER_PIN untouched).
let attributionSource: "AUTO" | "USER_PIN" | undefined;
// Set when the band slot is held by a different dose and this take is
// recorded unscheduled; it must not converge onto any slot row below.
let heldByAnotherDose = false;
if (isExplicitTaken && entry.forceSlotInstant !== undefined) {
canonicalSlot = await resolveForcedSlotForWrite({
userId: user.id,
@@ -486,6 +493,26 @@ async function postBulk(request: NextRequest): Promise<Response> {
});
canonicalSlot = attribution.slotInstant;
attributionSource = "AUTO";
// An external dose record (a HealthKit dose event) whose band slot
// already carries a different recorded dose is an extra dose, not a
// correction of that one. Converging would overwrite the slot's
// take last-write-wins and the extra dose would exist nowhere; it is
// recorded as its own unscheduled dose instead.
if (
canonicalSlot !== null &&
entry.externalId !== undefined &&
(await slotHeldByAnotherDose({
userId: user.id,
medicationId: entry.medicationId,
canonicalSlot,
takenAt: entry.takenAt as Date,
externalId: entry.externalId,
}))
) {
canonicalSlot = null;
heldByAnotherDose = true;
extraDosesUnscheduled += 1;
}
} else if (isExplicitSkip && entry.scheduledFor === undefined) {
// v1.16.9 — a slot-less deliberate skip resolves by band
// membership on the skip moment, exactly like a take does. The
@@ -666,6 +693,7 @@ async function postBulk(request: NextRequest): Promise<Response> {
// late-morning dose consuming the evening slot). It records
// standalone (ad-hoc) instead.
const existingSlotRow =
!heldByAnotherDose &&
entry.scheduledFor !== undefined &&
mayConvergeOntoSuppliedSlot({
skipped: entry.skipped,
@@ -878,6 +906,7 @@ async function postBulk(request: NextRequest): Promise<Response> {
updated,
duplicates,
skipped: skipped.length,
extra_doses_unscheduled: extraDosesUnscheduled,
...(unstableShapes.length > 0
? unstableExternalIdMeta("medication.intake.bulk", unstableShapes)
: {}),