feat(labs): merge markers, keep aliases and read lab series per marker

POST /api/biomarkers/{id}/merge folds markers into one in a transaction,
converting readings where the registry knows the units and refusing the
whole merge where it does not; a dry run answers the same result and
writes nothing. Aliases appear on the marker and are edited through the
marker's PUT. Scan and document review rows name the matched marker or
offer up to three, and a chosen marker is remembered as an alias. The
doctor report, FHIR and MCP group readings by marker. Aliases and the
merge ledger travel through backup and restore.
This commit is contained in:
Marc-André Bombeck committed 2026-10-15 22:22:40 +02:00
1 parent a3ce6af9a3
commit 81be73fca8
42 files changed
+1645 -423

No files matched your search

+8 -8
View File
@@ -9905,11 +9905,11 @@ model BiomarkerAlias {
id String @id @default(cuid())
userId String @map("user_id")
biomarkerId String @map("biomarker_id")
/// @pending-consumer(#v143-B3) The alias as written, shown on the marker.
/// The alias as written, shown on the marker.
name String @db.VarChar(120)
/// @pending-consumer(#v143-B3) `foldForMatch(name)`, the match key; unique per account.
/// `foldForMatch(name)`, the match key; unique per account.
nameFolded String @map("name_folded") @db.VarChar(120)
/// @pending-consumer(#v143-B3) `MERGE`, `IMPORT_CHOICE` or `MANUAL` (CHECK in migration 0403).
/// `MERGE`, `IMPORT_CHOICE` or `MANUAL` (CHECK in migration 0403).
source String
createdAt DateTime @default(now()) @map("created_at")
@@ -9927,15 +9927,15 @@ model BiomarkerAlias {
model BiomarkerMerge {
id String @id @default(cuid())
userId String @map("user_id")
/// @pending-consumer(#v143-B3) The marker the source was folded into.
/// The marker the source was folded into.
targetBiomarkerId String @map("target_biomarker_id")
/// @pending-consumer(#v143-B3) Id of the deleted source marker (no FK: the row is gone).
/// Id of the deleted source marker (no FK: the row is gone).
sourceBiomarkerId String @map("source_biomarker_id")
/// @pending-consumer(#v143-B3) The source marker's name at merge time.
/// The source marker's name at merge time.
sourceName String @map("source_name") @db.VarChar(120)
/// @pending-consumer(#v143-B3) How many readings moved from the source.
/// How many readings moved from the source.
readingCount Int @map("reading_count")
/// @pending-consumer(#v143-B3) When the merge ran.
/// When the merge ran.
mergedAt DateTime @default(now()) @map("merged_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)