mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 14:40:52 +00:00
A document arriving in the vault and a lab panel being committed both ask the same question — does this belong to a visit? — and the answer has to be the same question asked once. The rule lives in one module with one exported window constant: the caller's live DONE or PLANNED visits within seven days of an anchor date, resolved into a verdict rather than a list. One candidate pre-selects, visibly, with one undo. Two or more offer a picker with nothing pre-selected: pre-selecting the first of two is what teaches a person to stop reading suggestions, and the return type has no field on that arm a caller could mistake for a choice. Zero offers nothing. Cancelled visits and no-shows are not candidates. Neither happened, so neither produced the letter or the panel being filed, and admitting them would widen the set with rows that are wrong most of the time — which pushes the honest single match into the picker branch. The verdict is served over HTTP because all three moments live in the browser. Handing the client a windowed list and letting it decide would put the rule in two places, and the copy in the browser is the one nobody re-reads. - suggest-window.ts with the window as an exported constant, not a literal - GET /api/encounters/suggest, profile domain, reviewed into the frozen literal - OpenAPI regenerated - fixtures are relative to the run instant; a fixed date slides out of a relative window and rots the test into a false green
60 lines
2.1 KiB
TypeScript
60 lines
2.1 KiB
TypeScript
/**
|
|
* `GET /api/encounters/suggest?anchor=<iso>` — which visit does a record dated
|
|
* `anchor` most plausibly belong to?
|
|
*
|
|
* The three capture moments that ask this question live in the browser: the
|
|
* document upload review, the lab extraction review and the manual lab form.
|
|
* The rule they ask about — the ±7-day window, the DONE / PLANNED filter, and
|
|
* the one-pre-selects / two-offer-a-picker verdict — lives in
|
|
* `src/lib/encounters/suggest-window.ts` and is answered here so all three read
|
|
* the same answer. Handing the client a windowed list and letting it decide
|
|
* would put the rule in two places, and the copy in the browser is the one
|
|
* nobody re-reads when the rule changes.
|
|
*
|
|
* A read of the record's own visits, so it declares the same domain the visit
|
|
* list does. `userId` is narrowed from auth; the anchor is the only input and
|
|
* it is a date, not an id.
|
|
*/
|
|
import { NextRequest } from "next/server";
|
|
|
|
import { prisma } from "@/lib/db";
|
|
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
|
|
import { annotate } from "@/lib/logging/context";
|
|
import { apiSuccess, returnAllZodIssues } from "@/lib/api-response";
|
|
import { encounterSuggestQuerySchema } from "@/lib/validations/encounters";
|
|
import { suggestEncounterForDate } from "@/lib/encounters/suggest-window";
|
|
|
|
export const GET = apiHandler(async (request: NextRequest) => {
|
|
const { user } = await requireRecordAuth("read", "profile");
|
|
|
|
const params = new URL(request.url).searchParams;
|
|
const parsed = encounterSuggestQuerySchema.safeParse({
|
|
anchor: params.get("anchor") ?? undefined,
|
|
});
|
|
if (!parsed.success) {
|
|
return returnAllZodIssues(parsed.error, 422, {
|
|
errorCode: "encounter.invalid",
|
|
});
|
|
}
|
|
|
|
const result = await suggestEncounterForDate(prisma, {
|
|
userId: user.id,
|
|
anchor: new Date(parsed.data.anchor),
|
|
});
|
|
|
|
annotate({
|
|
action: { name: "encounter.visit.suggest", entity_type: "encounter" },
|
|
meta: {
|
|
verdict: result.kind,
|
|
candidates:
|
|
result.kind === "many"
|
|
? result.encounters.length
|
|
: result.kind === "one"
|
|
? 1
|
|
: 0,
|
|
},
|
|
});
|
|
|
|
return apiSuccess(result);
|
|
});
|