feat(labs): draft review, confirm with normalization, discard
- Add ConfirmDraftBody schema to shared types - Implement GET /api/labs/drafts/:id (retrieves draft with extracted data) - Implement POST /api/labs/drafts/:id/confirm (normalizes markers, inserts lab draw + biomarkers in transaction, marks draft confirmed) - Implement POST /api/labs/drafts/:id/discard (marks draft discarded) - Add comprehensive test suite with 3 new tests Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,9 +4,11 @@ import { randomUUID } from "node:crypto";
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import type { Db } from "../db";
|
||||
import { labDrafts } from "../db/schema";
|
||||
import { biomarkers, labDrafts, labDraws } from "../db/schema";
|
||||
import { extractFromText } from "../lib/extract";
|
||||
import { normalizeMarker } from "../lib/normalize";
|
||||
import { pdfToText } from "../lib/pdf";
|
||||
import { ConfirmDraftBody } from "@helios/shared";
|
||||
|
||||
export type LabsDeps = { db: Db; key: Buffer; dataDir: string; llmFetch?: typeof fetch };
|
||||
|
||||
@@ -64,5 +66,50 @@ export function labsRoutes(deps: LabsDeps) {
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/labs/drafts/:id", async (c) => {
|
||||
const row = (await deps.db.select().from(labDrafts).where(eq(labDrafts.id, c.req.param("id")))).at(0);
|
||||
if (!row) return c.json({ error: "not found" }, 404);
|
||||
return c.json({
|
||||
id: row.id, filename: row.filename, status: row.status, error: row.error,
|
||||
draft: row.extracted ? JSON.parse(row.extracted) : null,
|
||||
});
|
||||
});
|
||||
|
||||
app.post("/labs/drafts/:id/confirm", async (c) => {
|
||||
const row = (await deps.db.select().from(labDrafts).where(eq(labDrafts.id, c.req.param("id")))).at(0);
|
||||
if (!row) return c.json({ error: "not found" }, 404);
|
||||
if (row.status !== "pending") return c.json({ error: "draft is not pending" }, 409);
|
||||
const body = ConfirmDraftBody.safeParse(await c.req.json().catch(() => null));
|
||||
if (!body.success) return c.json({ error: body.error.issues[0]?.message ?? "invalid body" }, 400);
|
||||
|
||||
const drawId = randomUUID();
|
||||
const norm = body.data.markers.map((m) => normalizeMarker({
|
||||
panel: m.panel, name: m.name, value: m.value, unit: m.unit,
|
||||
referenceRange: m.referenceRange, flagged: m.flagged,
|
||||
}));
|
||||
await deps.db.transaction(async (tx) => {
|
||||
await tx.insert(labDraws).values({
|
||||
id: drawId, collectedAt: body.data.collectedDate, labName: body.data.labName,
|
||||
draftId: row.id, createdAt: Date.now(),
|
||||
});
|
||||
for (const n of norm) {
|
||||
await tx.insert(biomarkers).values({
|
||||
drawId, panel: n.panel, name: n.name, marker: n.marker, analyteKey: n.analyteKey,
|
||||
value: n.value, valueNum: n.valueNum, unit: n.unit, referenceRange: n.referenceRange,
|
||||
flagged: n.flagged ? 1 : 0, valueCanonical: n.valueCanonical, canonicalUnit: n.canonicalUnit,
|
||||
});
|
||||
}
|
||||
await tx.update(labDrafts).set({ status: "confirmed" }).where(eq(labDrafts.id, row.id));
|
||||
});
|
||||
return c.json({ drawId }, 201);
|
||||
});
|
||||
|
||||
app.post("/labs/drafts/:id/discard", async (c) => {
|
||||
const row = (await deps.db.select().from(labDrafts).where(eq(labDrafts.id, c.req.param("id")))).at(0);
|
||||
if (!row) return c.json({ error: "not found" }, 404);
|
||||
await deps.db.update(labDrafts).set({ status: "discarded" }).where(eq(labDrafts.id, row.id));
|
||||
return c.body(null, 204);
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user