@openhealth/oht-custom-parser-lib 0.2.81 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,693 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.ohtReportMeasurementsExtractor = ohtReportMeasurementsExtractor;
40
+ const dataUtils_1 = require("../util-ts/dataUtils");
41
+ const extractionUtils_1 = require("../util-ts/extractionUtils");
42
+ const pinoLogger_1 = __importDefault(require("../util-ts/pinoLogger"));
43
+ const apiUtils_1 = require("../util-ts/apiUtils");
44
+ const oht_types_1 = require("../types/oht.types");
45
+ const transformationRules_service_1 = require("./transformationRules.service");
46
+ const auxiliaryFunctions_service_1 = require("./auxiliaryFunctions.service");
47
+ const path = __importStar(require("path"));
48
+ const fs = __importStar(require("fs"));
49
+ /**
50
+ * Logger instance for the application
51
+ */
52
+ const logger = (0, pinoLogger_1.default)();
53
+ // Import version from package.json file
54
+ // Handle multiple possible paths: compiled (dist/service/), development (service/), or when used as dependency
55
+ function getPackageJsonVersion() {
56
+ const possiblePaths = [
57
+ path.resolve(__dirname, '../../package.json'), // Compiled: dist/service/ -> dist/ -> root
58
+ path.resolve(__dirname, '../package.json'), // Development: service/ -> root
59
+ path.resolve(__dirname, '../../../package.json'), // When used as dependency from node_modules
60
+ path.resolve(process.cwd(), 'package.json'), // From current working directory
61
+ ];
62
+ for (const packagePath of possiblePaths) {
63
+ if (fs.existsSync(packagePath)) {
64
+ try {
65
+ const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
66
+ if (packageJson.name === '@openhealth/oht-custom-parser-lib' && packageJson.version) {
67
+ return packageJson.version;
68
+ }
69
+ }
70
+ catch (error) {
71
+ // Continue to next path if this one fails
72
+ continue;
73
+ }
74
+ }
75
+ }
76
+ // Fallback to a default version if package.json cannot be found
77
+ logger.warn('Could not find package.json, using default version');
78
+ return '0.0.0';
79
+ }
80
+ const ohtCustomParserLibVersion = getPackageJsonVersion();
81
+ /**
82
+ * Environment variable for OHT Core
83
+ */
84
+ const OHT_CORE_URL = process.env.OHT_CORE_URL;
85
+ /**
86
+ * Current filename being processed (global state for error reporting)
87
+ */
88
+ let filename = '';
89
+ /**
90
+ * Error messages for parser operations
91
+ */
92
+ const ERROR_MESSAGES = {
93
+ NO_LAB_KEYS_FOUND: 'No lab keys found',
94
+ NO_MAPPING_FOUND: 'No Mapping found for labKey',
95
+ };
96
+ /**
97
+ * Apply transformation rules to exams
98
+ * This function applies transformation rules chain to each exam, passing all exams as context
99
+ *
100
+ * @param exams - Array of original exam objects
101
+ * @param transformationRules - Mapping of labKey to transformation rule chains
102
+ * @param patientInfo - Patient information
103
+ * @param documentDate - Document date
104
+ * @returns Promise<ApplyTransformationRulesResult> - Array of transformed exams and error messages
105
+ */
106
+ async function applyTransformationRulesToExams(exams, transformationRules, patientInfo, documentDate) {
107
+ if (transformationRules.size === 0) {
108
+ return {
109
+ transformedExams: exams,
110
+ applicationErrorMessages: []
111
+ };
112
+ }
113
+ const transformedExams = [];
114
+ // Prepare context for transformation rules
115
+ const reportContext = {
116
+ patient: {
117
+ sex: patientInfo.sex,
118
+ birthdate: patientInfo.birthdate,
119
+ age: (0, auxiliaryFunctions_service_1.getPatientAge)(documentDate, patientInfo.birthdate),
120
+ }
121
+ };
122
+ // Prepare the OHT exam data
123
+ const ohtReport = {
124
+ exams: exams.map(e => e.ohtExam),
125
+ };
126
+ logger.info({ examsCount: exams.length, chainsCount: transformationRules.size, filename }, 'Applying transformation rules to exams...');
127
+ let labKeysWithRulesCount = 0;
128
+ let transformedLabKeysCount = 0;
129
+ const applicationErrorMessages = [];
130
+ for (const exam of exams) {
131
+ const labKey = exam.ohtExam.labKey;
132
+ const rulesChain = transformationRules.get(labKey);
133
+ if (!rulesChain || rulesChain.length === 0) {
134
+ transformedExams.push(exam);
135
+ continue;
136
+ }
137
+ labKeysWithRulesCount++;
138
+ try {
139
+ const result = await (0, transformationRules_service_1.applyRules)(rulesChain, exam.ohtExam, {
140
+ ohtReportJson: ohtReport,
141
+ contextJson: reportContext,
142
+ });
143
+ if (result.errors.length > 0) {
144
+ logger.warn({ labKey, errors: result.errors, filename }, 'Errors applying transformation rule(s) for labKey.');
145
+ applicationErrorMessages.push(`${labKey}: ${result.errors.map(e => `(rule ID ${e.ruleId}) ${e.error}`).join('; ')}`);
146
+ }
147
+ else {
148
+ if (result.appliedRules.length > 0) {
149
+ transformedLabKeysCount++;
150
+ }
151
+ logger.info({ labKey, appliedRules: result.appliedRules.length, filename }, 'Done applying transformation rule(s) for labKey.');
152
+ // Add the transformed exam to the result array only if there are no errors in the chain
153
+ transformedExams.push({
154
+ ohtExam: result.data,
155
+ originalExam: exam.originalExam
156
+ });
157
+ }
158
+ }
159
+ catch (error) {
160
+ logger.error({ labKey, error, filename }, 'Failed to apply transformation rules for labKey.');
161
+ applicationErrorMessages.push(`${labKey}: Unhandled error occurred.`);
162
+ }
163
+ }
164
+ logger.info({
165
+ filename,
166
+ examsCount: exams.length,
167
+ chainsCount: transformationRules.size,
168
+ transformedLabKeysCount,
169
+ labKeysWithRulesCount,
170
+ }, 'Done applying the transformation rules to exams.');
171
+ return {
172
+ transformedExams,
173
+ applicationErrorMessages
174
+ };
175
+ }
176
+ /**
177
+ * Extracts measurements from OHT report exams by processing each exam through biomarker mapping,
178
+ * transformation rules, and value extraction.
179
+ *
180
+ * This function is the main entry point for converting raw exam data into standardized OHT measurements.
181
+ * It handles biomarker mapping, unit extraction, value parsing, range extraction, and error reporting.
182
+ *
183
+ * ## Processing Flow:
184
+ *
185
+ * ### Step 1: Initialization and Validation
186
+ * - Validates that exams array is not empty
187
+ * - Initializes result arrays (measurements, unknownMeasurements, unknownUnits, unmappedLabKeys)
188
+ * - Sets up error tracking and transformation rules error tracking
189
+ *
190
+ * ### Step 2: Fetch Biomarker Mappings
191
+ * - Calls `getMappedBiomarkers()` to retrieve biomarker mappings from OHT Core backend
192
+ * - Maps labKeys to OHT biomarker IDs (ohtBmId) for the given partner
193
+ * - Logs the number of mappings retrieved
194
+ *
195
+ * ### Step 3: Apply Transformation Rules
196
+ * - Applies transformation rules to all exams using `applyTransformationRulesToExams()`
197
+ * - Transformation rules can modify exam data before processing (e.g., normalize units, fix values)
198
+ * - Creates a map of transformed exams for quick lookup during processing
199
+ * - Collects and reports any transformation rule errors
200
+ *
201
+ * ### Step 4: Collect Biomarker IDs and Fetch Biomarker Details
202
+ * - Iterates through all exams to collect unique OHT biomarker IDs from mappings
203
+ * - Filters out ignored biomarkers and unmapped biomarkers (ohtBmId === -1)
204
+ * - Makes API call to OHT Core to fetch full biomarker details (standard units, ranges, etc.)
205
+ * - Fetches synonym units from API if partnerId is provided (for unit resolution)
206
+ *
207
+ * ### Step 5: Process Each Exam
208
+ * For each exam in the input array:
209
+ *
210
+ * #### 5.1: Create UnmappedLabKey (Always)
211
+ * - **Always** creates an `unmappedLabKey` entry for every exam, regardless of mapping status
212
+ * - This ensures all labKeys are tracked for unmapped biomarker analysis
213
+ * - Includes exam metadata: labKey, biomarkerName, value, unit, annotation, originalName, dataOrigin
214
+ *
215
+ * #### 5.2: Check Biomarker Mapping Status
216
+ * - Retrieves backend mapping for the exam's labKey
217
+ * - Skips processing if biomarker is marked as `isIgnored === true`
218
+ * - Uses `handleBiomarkerId()` to determine final `ohtBmId` and `annotationConfig`
219
+ * - Handles unmapped biomarkers (ohtBmId === -1)
220
+ * - Handles `keepAsUnmapped` flag override
221
+ *
222
+ * #### 5.3: Process Mapped Biomarkers (ohtBmId !== -1 and !== undefined)
223
+ *
224
+ * ##### 5.3.1: Duplicate Check
225
+ * - Checks if biomarker ID already exists in measurements array
226
+ * - If duplicate, logs warning and adds error message, then continues to next exam
227
+ *
228
+ * ##### 5.3.2: Fetch Biomarker Details
229
+ * - Finds biomarker object from API response using `ohtBmId`
230
+ * - If not found, logs warning, adds error, and continues to next exam
231
+ *
232
+ * ##### 5.3.3: Extract Unit
233
+ * - Calls `ohtReportExtractUnit()` to extract and validate unit
234
+ * - Handles arbitrary units, standard units, alternative units, and synonym units
235
+ * - If unit cannot be resolved, adds to `unknownUnits` array
236
+ * - Logs warning and marks measurement as incorrectly parsed if unit extraction fails
237
+ *
238
+ * ##### 5.3.4: Parse Exam Value
239
+ * - Calls `ohtReportParseExamValue()` to extract value from exam
240
+ * - Determines value type (NUMERICAL or ALPHANUMERICAL) based on biomarker configuration
241
+ * - Handles greater than/less than comparators (e.g., ">10", "<5")
242
+ * - Logs warning and marks measurement as incorrectly parsed if value extraction fails
243
+ *
244
+ * ##### 5.3.5: Handle Numerical Without Valid Result
245
+ * - Checks if value is numerical but cannot be parsed as a valid number
246
+ * - If true, creates `unknownMeasurement` and continues to next exam
247
+ *
248
+ * ##### 5.3.6: Validate Plausible Values (for NUMERICAL biomarkers)
249
+ * - Calls `ohtReportCheckValueForPlausibleValues()` to validate value is within expected range
250
+ * - Converts value to standard unit if necessary using conversion factors
251
+ * - Logs warning and marks measurement as incorrectly parsed if value is not plausible
252
+ *
253
+ * ##### 5.3.7: Extract Reference Ranges (for NUMERICAL biomarkers)
254
+ * - Calls `ohtReportExtractReferenceRanges()` which uses `parseRange()` internally
255
+ * - Extracts min/max normal values based on patient age, sex, and range data
256
+ * - Filters ranges by patient demographics and selects most appropriate range
257
+ * - Logs warning and marks measurement as incorrectly parsed if range extraction fails
258
+ *
259
+ * ##### 5.3.8: Extract Reference Annotation
260
+ * - Calls `ohtReportExtractReferenceAnnotation()` to extract annotation text
261
+ * - Handles appended mappings (combines values from multiple related biomarkers)
262
+ * - Merges annotation from exam data with appended mapping results
263
+ *
264
+ * ##### 5.3.9: Build Measurement Object
265
+ * - Constructs complete `Measurement` object with all extracted data:
266
+ * - Biomarker ID, value type, value, alphanumeric value
267
+ * - Unit, original unit, arbitrary unit flag
268
+ * - Reference range (min/max normal values)
269
+ * - Range annotation
270
+ * - Material, method
271
+ * - Collection dates (collectionDate, effectiveDate, examCollectionDate, examEffectiveDate)
272
+ * - Original name for document search
273
+ * - Digitalization approval flags
274
+ * - Adds measurement to `measurements` array
275
+ *
276
+ * #### 5.4: Process Unmapped Biomarkers (no mapping or ohtBmId === -1)
277
+ * - Creates `unknownMeasurement` object with:
278
+ * - LabKey, biomarker name (mnemonic:examName), value, unit, annotation
279
+ * - Data origin information
280
+ * - Original exam data for reference
281
+ * - Adds to `unknownMeasurements` array
282
+ *
283
+ * ### Step 6: Summary and Return
284
+ * - Logs processing summary (total labKeys, mapped biomarkers, unmapped biomarkers, errors)
285
+ * - Returns result object containing:
286
+ * - `measurements`: Array of successfully parsed and mapped measurements
287
+ * - `unknownMeasurements`: Array of measurements that could not be mapped or parsed
288
+ * - `unknownUnits`: Array of units that could not be resolved
289
+ * - `unmappedLabKeys`: Array of all labKeys (always created for every exam)
290
+ * - `isReportCorrectlyParsed`: Boolean indicating if all measurements were parsed correctly
291
+ * - `transformationRulesErrors`: Array of errors from transformation rules application
292
+ *
293
+ * ## Error Handling:
294
+ * - All errors are added to the `errorMessages` array passed as parameter
295
+ * - Errors are categorized by destination (slack, notes, bi)
296
+ * - Processing continues even if individual exams fail
297
+ * - Errors are logged with context (filename, labKey, ohtBmId, etc.)
298
+ *
299
+ * ## Key Behaviors:
300
+ * - **UnmappedLabKeys**: Always created for ALL exams, regardless of mapping status
301
+ * - **UnknownMeasurements**: Created when:
302
+ * - No backend mapping exists for labKey
303
+ * - ohtBmId === -1 (explicitly unmapped)
304
+ * - Value is numerical but cannot be parsed as valid number
305
+ * - **Measurements**: Created only when biomarker is successfully mapped and all extraction steps succeed
306
+ *
307
+ * @param exams - Array of Exam objects containing ohtExam and originalExam data
308
+ * @param sourceFilename - Name of the source file being processed (for error reporting)
309
+ * @param patientInfo - Patient information (sex, birthdate) for range extraction
310
+ * @param documentDate - Document date for age calculation and range extraction
311
+ * @param transformationRules - Map of labKey to transformation rule chains to apply
312
+ * @param ohtCoreApiKey - API key for OHT Core API calls
313
+ * @param partnerId - Partner ID for fetching biomarker mappings and synonym units
314
+ * @param errorMessages - Array to accumulate error messages (modified in-place)
315
+ * @returns Promise resolving to extraction result with measurements, unknownMeasurements, unknownUnits, unmappedLabKeys, parsing status, and transformation errors
316
+ */
317
+ async function ohtReportMeasurementsExtractor(exams, sourceFilename, patientInfo, documentDate, transformationRules, ohtCoreApiKey, partnerId, errorMessages) {
318
+ filename = sourceFilename;
319
+ const measurements = [];
320
+ const unknownMeasurements = [];
321
+ const unknownUnits = [];
322
+ const unmappedLabKeys = [];
323
+ let isReportCorrectlyParsed = true;
324
+ let transformationRulesErrors = [];
325
+ // Check if we have exams
326
+ const labKeys = exams.map(e => e.ohtExam.labKey);
327
+ if (labKeys.length === 0) {
328
+ const errorMsg = `No lab keys found, filename: ${filename}`;
329
+ logger.error(errorMsg);
330
+ errorMessages.push({
331
+ destination: { slack: true, notes: false, bi: false },
332
+ message: errorMsg,
333
+ });
334
+ return {
335
+ measurements,
336
+ unknownMeasurements,
337
+ unknownUnits,
338
+ unmappedLabKeys,
339
+ isReportCorrectlyParsed: false,
340
+ transformationRulesErrors,
341
+ };
342
+ }
343
+ logger.info(`Processing lab file with ${labKeys.length} keys, filename: ${filename}`);
344
+ // Call getMappedBiomarkers to get the biomarker mapping for the partner
345
+ const mappedBiomarkers = await (0, dataUtils_1.getMappedBiomarkers)(partnerId, ohtCoreApiKey);
346
+ // add "GROUPED_" bms for appended mappings
347
+ if (mappedBiomarkers) {
348
+ const groupedLabKeys = Object.keys(mappedBiomarkers).filter(labKey => labKey.startsWith('GROUPED_'));
349
+ for (const groupedLabKey of groupedLabKeys) {
350
+ const emptyOhtExam = {
351
+ labKey: groupedLabKey,
352
+ value: mappedBiomarkers[groupedLabKey]?.syntheticValue ?? '',
353
+ unit: '',
354
+ documentDate: documentDate ? documentDate.toISOString() : '',
355
+ description: '',
356
+ mnemonic: '',
357
+ examName: '',
358
+ range: [],
359
+ annotation: '',
360
+ };
361
+ const emptyExam = {
362
+ ohtExam: emptyOhtExam,
363
+ originalExam: {
364
+ contentType: 'application/json',
365
+ exam: JSON.stringify(emptyOhtExam),
366
+ },
367
+ };
368
+ exams.push(emptyExam);
369
+ }
370
+ if (groupedLabKeys.length > 0) {
371
+ logger.info(`Added ${groupedLabKeys.length} empty OhtExam objects for GROUPED_ biomarkers to check for appended mappings`);
372
+ }
373
+ }
374
+ if (mappedBiomarkers) {
375
+ logger.info(`Retrieved ${Object.keys(mappedBiomarkers).length} biomarker mappings from backend for partner: ${partnerId}`);
376
+ }
377
+ else {
378
+ logger.info(`No biomarker mappings found in backend for partner: ${partnerId}`);
379
+ }
380
+ // Apply transformation rules to all exams
381
+ let transformedExamsResult = { transformedExams: [], applicationErrorMessages: [] };
382
+ try {
383
+ transformedExamsResult = await applyTransformationRulesToExams(exams, transformationRules, patientInfo, documentDate);
384
+ }
385
+ catch (error) {
386
+ logger.error({ error, filename }, 'Unhandled error while applying transformation rules to exams.');
387
+ errorMessages.push({
388
+ destination: { slack: true, notes: true, bi: false },
389
+ message: `Failed applying the transformation rules for filename ${filename}. ${error instanceof Error ? error.message : error}`,
390
+ });
391
+ }
392
+ if (transformedExamsResult.applicationErrorMessages.length > 0) {
393
+ transformationRulesErrors = transformedExamsResult.applicationErrorMessages;
394
+ errorMessages.push({
395
+ destination: { slack: false, notes: true, bi: false },
396
+ message: `Failed applying some transformation rules:\n${transformedExamsResult.applicationErrorMessages.map((e) => ` - ${e}`).join("\n\n")}`,
397
+ });
398
+ }
399
+ const transformedExams = transformedExamsResult.transformedExams;
400
+ const transformedExamsMap = transformedExams.reduce((map, exam) => map.set(exam.ohtExam.labKey, exam), new Map());
401
+ // Collect all possible OHT biomarker IDs from mapped biomarkers
402
+ const allPossibleOHTBmIds = [];
403
+ for (const exam of exams) {
404
+ const labKey = exam.ohtExam.labKey;
405
+ const backendMapping = mappedBiomarkers?.[labKey];
406
+ if (backendMapping && !backendMapping.isIgnored) {
407
+ const { ohtBmId } = (0, extractionUtils_1.handleBiomarkerId)(backendMapping, labKey);
408
+ if (ohtBmId && ohtBmId !== -1) {
409
+ allPossibleOHTBmIds.push(ohtBmId);
410
+ }
411
+ }
412
+ }
413
+ // data to request api
414
+ const requestData = {
415
+ "biomarkerIds": allPossibleOHTBmIds,
416
+ "page": 1,
417
+ "pageSize": allPossibleOHTBmIds.length
418
+ };
419
+ // api call to ohtCore to find biomarkers.
420
+ const biomarkers = await (0, apiUtils_1.makeApiCallWithRetry)('post', `${OHT_CORE_URL}/v1/biomarker/biomarkers`, requestData, ohtCoreApiKey);
421
+ // Fetch synonym units from API if enabled
422
+ let synonymUnits = null;
423
+ if (partnerId) {
424
+ synonymUnits = await (0, dataUtils_1.fetchSynonymUnits)(partnerId, ohtCoreApiKey);
425
+ // If API fetch failed or returned empty, set to null to use hardcoded fallback
426
+ if (synonymUnits.length === 0) {
427
+ synonymUnits = null;
428
+ }
429
+ }
430
+ // Process each exam directly
431
+ for (const exam of exams) {
432
+ const labKey = exam.ohtExam.labKey;
433
+ const examObj = exam.ohtExam;
434
+ const originalExam = exam.originalExam;
435
+ // Get the transformed exam for this labKey, or use the original ohtExam
436
+ const transformedExam = transformedExamsMap.get(labKey);
437
+ const transformedExamObj = transformedExam ? transformedExam.ohtExam : examObj;
438
+ // ALWAYS create unmappedLabKey for all exams
439
+ const unmappedLabKey = {
440
+ labKey: labKey,
441
+ biomarkerName: `${examObj.mnemonic}`,
442
+ value: examObj.value || '',
443
+ unit: examObj.unit || '',
444
+ annotation: examObj.annotation || '',
445
+ originalName: `${examObj.examName}`,
446
+ dataOrigin: [
447
+ {
448
+ itemExtracted: 'measurement',
449
+ method: 'custom-parser',
450
+ methodVersion: `oht-custom-parser-lib-v${ohtCustomParserLibVersion}`,
451
+ path: 'exams',
452
+ },
453
+ ],
454
+ originalExam: originalExam ? originalExam : undefined,
455
+ ohtExam: examObj,
456
+ };
457
+ unmappedLabKeys.push(unmappedLabKey);
458
+ // Get backend mapping for this labKey
459
+ const backendMapping = mappedBiomarkers?.[labKey];
460
+ // Skip processing if biomarker is marked as ignored
461
+ if (backendMapping?.isIgnored === true) {
462
+ logger.info(`Skipping ignored biomarker: ${labKey}`);
463
+ continue;
464
+ }
465
+ // Handle biomarker id
466
+ let finalOhtBmId;
467
+ let annotationConfig;
468
+ if (backendMapping) {
469
+ const { ohtBmId, annotationConfig: config } = (0, extractionUtils_1.handleBiomarkerId)(backendMapping, labKey);
470
+ finalOhtBmId = ohtBmId;
471
+ annotationConfig = {
472
+ enabled: config.enabled,
473
+ labKeys: backendMapping.annotationConfig?.labKeys || [],
474
+ maxLength: backendMapping.annotationConfig?.maxLength,
475
+ fallbackValue: backendMapping.annotationConfig?.fallbackValue,
476
+ separator: backendMapping.annotationConfig?.separator,
477
+ };
478
+ logger.info(`Using mappings from custom parser studio (oht-core backend) for labKey: ${labKey} (ohtBmId: ${backendMapping.ohtBmId})`);
479
+ }
480
+ else {
481
+ logger.debug(`No backend mapping found for labKey: ${labKey}, will process as unmapped`);
482
+ }
483
+ // Process as mapped biomarker if ohtBmId is valid
484
+ if (finalOhtBmId !== undefined && finalOhtBmId !== -1) {
485
+ let isMeasurementCorrectlyParsed = true;
486
+ // if BM is already in measurements, continue to the next one
487
+ if (measurements.some(measurement => measurement.biomarkerId === finalOhtBmId)) {
488
+ logger.warn({ ohtBmId: finalOhtBmId, labKey }, `Biomarker already in measurements array, should investigate, filename: ${filename}`);
489
+ errorMessages.push({
490
+ destination: { slack: false, notes: false, bi: true },
491
+ message: `Biomarkers Duplicated keys:[PLACEHOLDER]`,
492
+ item: `bmId:${finalOhtBmId}, labKey:${transformedExamObj.labKey}`
493
+ });
494
+ continue;
495
+ }
496
+ const ohtBiomarker = biomarkers?.data?.items.find((bm) => bm.id === finalOhtBmId);
497
+ if (!ohtBiomarker) {
498
+ logger.warn({ ohtBmId: finalOhtBmId, labKey }, `Biomarker not found in API response for ohtBmId: ${finalOhtBmId}, labKey: ${transformedExamObj.labKey}, filename: ${filename}`);
499
+ errorMessages.push({
500
+ destination: { slack: true, notes: true, bi: true },
501
+ message: `Biomarker not found for:[PLACEHOLDER]`,
502
+ item: `bmId:${finalOhtBmId}, labKey:${transformedExamObj.labKey}`
503
+ });
504
+ continue;
505
+ }
506
+ // Used for error messages
507
+ const bmInfo = ohtBiomarker?.acf?.shortName ?
508
+ `name:${ohtBiomarker.acf.shortName}, bmId:${finalOhtBmId}, labKey:${transformedExamObj.labKey}` :
509
+ `bmId:${finalOhtBmId}, labKey:${transformedExamObj.labKey}`;
510
+ const { unit, isUnitParsedCorrectly, originalUnit, isArbitraryUnit, unknownUnit } = (0, extractionUtils_1.ohtReportExtractUnit)(transformedExamObj, ohtBiomarker, synonymUnits, partnerId);
511
+ if (unknownUnit.length > 0) {
512
+ unknownUnits.push({
513
+ biomarkerId: ohtBiomarker.id,
514
+ unit: unknownUnit,
515
+ partnerId: partnerId || '',
516
+ labKey: transformedExamObj.labKey,
517
+ ohtExam: examObj,
518
+ originalExam: originalExam,
519
+ biomarkerName: transformedExamObj.mnemonic || '',
520
+ dataImportFileUploadId: null, // is null it will be set later in the structParser.service.ts
521
+ });
522
+ }
523
+ if (!isUnitParsedCorrectly) {
524
+ logger.warn({ ohtBmId: finalOhtBmId, labKey }, `Unit not extracted correctly for "${finalOhtBmId}|${transformedExamObj.labKey}", filename: ${filename}`);
525
+ errorMessages.push({
526
+ destination: { slack: true, notes: true, bi: true },
527
+ message: `Unit not extracted correctly for:[PLACEHOLDER]`,
528
+ item: `${bmInfo}`
529
+ });
530
+ isMeasurementCorrectlyParsed = false;
531
+ }
532
+ let { valueType, alphanumericalValue, numberValue, valueComparator, isValueParsedCorrectly } = (0, extractionUtils_1.ohtReportParseExamValue)(transformedExamObj, ohtBiomarker);
533
+ if (!isValueParsedCorrectly) {
534
+ logger.warn({ ohtBmId: finalOhtBmId, labKey }, `Value not extracted correctly for "${finalOhtBmId}|${transformedExamObj.labKey}", filename: ${filename}`);
535
+ errorMessages.push({
536
+ destination: { slack: true, notes: true, bi: true },
537
+ message: `Value not extracted correctly for:[PLACEHOLDER]`,
538
+ item: `${bmInfo}`
539
+ });
540
+ isMeasurementCorrectlyParsed = false;
541
+ }
542
+ if ((0, dataUtils_1.isNumericalWithoutValidResult)(valueType, numberValue, alphanumericalValue)) {
543
+ const alphanumericalValueTrimmed = alphanumericalValue?.trim() ?? '';
544
+ const unknownMeasurement = {
545
+ biomarkerName: transformedExamObj.description ?? '',
546
+ value: alphanumericalValueTrimmed,
547
+ unit: unit ?? '',
548
+ annotation: transformedExamObj.annotation ?? '',
549
+ dataOrigin: [{
550
+ itemExtracted: 'measurement',
551
+ method: 'custom-parser',
552
+ methodVersion: 'oht-custom-parser-lib',
553
+ path: 'exams',
554
+ }],
555
+ originalName: transformedExamObj.examName ?? '',
556
+ originalExam: {
557
+ contentType: 'application/json',
558
+ exam: JSON.stringify(transformedExamObj)
559
+ },
560
+ ohtExam: transformedExamObj
561
+ };
562
+ unknownMeasurements.push(unknownMeasurement);
563
+ continue;
564
+ }
565
+ // Check if the value is plausible
566
+ if (isValueParsedCorrectly && valueType === 'NUMERICAL') {
567
+ const isPlausibleValue = numberValue !== null && (0, extractionUtils_1.ohtReportCheckValueForPlausibleValues)(finalOhtBmId, transformedExamObj.labKey, ohtBiomarker, unit, numberValue, isUnitParsedCorrectly, filename);
568
+ if (!isPlausibleValue) {
569
+ logger.warn({ ohtBmId: finalOhtBmId, labKey }, `Value [${numberValue} ${unit}] is not plausible for "${finalOhtBmId}|${transformedExamObj.labKey}", filename: ${filename}`);
570
+ errorMessages.push({
571
+ destination: { slack: true, notes: true, bi: true },
572
+ message: `Value is not plausible for:[PLACEHOLDER]`,
573
+ item: `${bmInfo}, value:${numberValue}${unit}`
574
+ });
575
+ isMeasurementCorrectlyParsed = false;
576
+ }
577
+ }
578
+ let from = null, to = null;
579
+ let isRangeParsedCorrectly = true;
580
+ // If the biomarker is numerical, extract the reference range
581
+ if ((0, dataUtils_1.isBiomarkerValueNumerical)(ohtBiomarker)) {
582
+ const extractedRange = (0, extractionUtils_1.ohtReportExtractReferenceRanges)(transformedExamObj, patientInfo);
583
+ from = extractedRange.from ?? null;
584
+ to = extractedRange.to ?? null;
585
+ isRangeParsedCorrectly = extractedRange.isRangeParsedCorrectly;
586
+ if (!isRangeParsedCorrectly) {
587
+ logger.warn({ ohtBmId: finalOhtBmId, labKey }, `Range not extracted correctly for "${finalOhtBmId}|${transformedExamObj.labKey}", filename: ${filename}`);
588
+ errorMessages.push({
589
+ destination: { slack: true, notes: true, bi: true },
590
+ message: `Range not extracted correctly for:[PLACEHOLDER]`,
591
+ item: `${bmInfo}`
592
+ });
593
+ isMeasurementCorrectlyParsed = false;
594
+ }
595
+ }
596
+ let extractedAnnotation;
597
+ // TODO: always extract annotation? or only if annotationConfig.enabled is true?
598
+ extractedAnnotation = (0, extractionUtils_1.ohtReportExtractReferenceAnnotation)(annotationConfig, transformedExamObj, patientInfo, documentDate, transformedExams, filename);
599
+ // Use material and method directly from ohtExam
600
+ const material = transformedExamObj?.material ?? '';
601
+ const method = transformedExamObj?.method ?? '';
602
+ let alphaValue = !(0, dataUtils_1.isBiomarkerValueNumerical)(ohtBiomarker) && alphanumericalValue
603
+ ? alphanumericalValue.trim()
604
+ : '';
605
+ // adding unit for alphanumerical values as number.
606
+ if (!Number.isNaN(parseFloat(alphaValue))) {
607
+ // Use unit directly from ohtExam
608
+ let originalUnitFromExam = transformedExamObj?.unit ?? '';
609
+ originalUnitFromExam = originalUnitFromExam?.replaceAll(/ /g, '') ?? '';
610
+ alphaValue = alphaValue && unit?.length > 0 ? `${alphaValue} ${originalUnitFromExam}`.trim() : alphaValue;
611
+ }
612
+ // adding dates for measurements in ISO 8601 format
613
+ const collectionDate = transformedExamObj.collectionDate ? new Date(transformedExamObj.collectionDate).toISOString() : null;
614
+ const effectiveDate = transformedExamObj.effectiveDate ? new Date(transformedExamObj.effectiveDate).toISOString() : null;
615
+ const examCollectionDate = transformedExamObj.examCollectionDate ? new Date(transformedExamObj.examCollectionDate).toISOString() : null;
616
+ const examEffectiveDate = transformedExamObj.examEffectiveDate ? new Date(transformedExamObj.examEffectiveDate).toISOString() : null;
617
+ // add original name for document search
618
+ const originalName = transformedExamObj.examName ?? '';
619
+ // if the biomarker is a grouped biomarker and the annotation is empty, ignore it and continue to the next one
620
+ if (labKey.startsWith('GROUPED_') && !extractedAnnotation) {
621
+ continue;
622
+ }
623
+ let measurement = {
624
+ biomarkerId: ohtBiomarker.id,
625
+ valueType,
626
+ value: (0, dataUtils_1.isBiomarkerValueNumerical)(ohtBiomarker) ? numberValue : null,
627
+ alphanumericValue: alphaValue,
628
+ rangeAnnotation: extractedAnnotation ? extractedAnnotation : null,
629
+ showVisualisationIfAnnotated: backendMapping?.referenceAsAnnotation?.enabled ?? false,
630
+ comparator: valueComparator ?? oht_types_1.MeasurementValueComparator.EQUAL,
631
+ minNormalValue: from,
632
+ maxNormalValue: to,
633
+ unit,
634
+ originalUnit,
635
+ isArbitraryUnit,
636
+ material,
637
+ method,
638
+ isDigitalizationApproved: isMeasurementCorrectlyParsed,
639
+ isManualChecked: isMeasurementCorrectlyParsed,
640
+ collectionDate: collectionDate,
641
+ effectiveDate: effectiveDate,
642
+ examCollectionDate: examCollectionDate,
643
+ examEffectiveDate: examEffectiveDate,
644
+ originalName: originalName,
645
+ };
646
+ measurements.push(measurement);
647
+ isReportCorrectlyParsed = isReportCorrectlyParsed && isMeasurementCorrectlyParsed;
648
+ }
649
+ else {
650
+ // No mapping or ohtBmId === -1: create unknownMeasurement
651
+ const labkey = transformedExamObj?.labKey?.trim() || labKey.split(':')[0];
652
+ const unknownMeasurementName = `${transformedExamObj.mnemonic}:${transformedExamObj.examName}`;
653
+ const originalName = labKey.split(':')[1] || transformedExamObj.examName;
654
+ const annotation = `${transformedExamObj.annotation || ''}`;
655
+ const dataOrigin = {
656
+ itemExtracted: 'measurement',
657
+ method: 'custom-parser',
658
+ methodVersion: `oht-custom-parser-lib-v${ohtCustomParserLibVersion}`,
659
+ path: 'exams',
660
+ };
661
+ const unknownMeasurement = {
662
+ biomarkerName: unknownMeasurementName,
663
+ value: transformedExamObj.value || '',
664
+ unit: transformedExamObj.unit || '',
665
+ annotation: annotation,
666
+ dataOrigin: [dataOrigin],
667
+ originalName: originalName,
668
+ originalExam: originalExam ? originalExam : undefined,
669
+ ohtExam: transformedExamObj,
670
+ };
671
+ unknownMeasurements.push(unknownMeasurement);
672
+ }
673
+ }
674
+ // Log summary of processing
675
+ const backendMappedCount = measurements.length;
676
+ const unmappedCount = unknownMeasurements.length;
677
+ logger.info(`Processing completed for ${filename}:`, {
678
+ totalLabKeys: labKeys.length,
679
+ backendMappedBiomarkers: backendMappedCount,
680
+ unmappedBiomarkers: unmappedCount,
681
+ totalProcessed: exams.length,
682
+ errors: errorMessages.length,
683
+ });
684
+ return {
685
+ measurements,
686
+ unknownMeasurements,
687
+ unknownUnits,
688
+ unmappedLabKeys,
689
+ isReportCorrectlyParsed,
690
+ transformationRulesErrors,
691
+ };
692
+ }
693
+ //# sourceMappingURL=ohtReportMeasurementsExtractor.service.js.map