Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
316c598
Adds NotesMetadata entity and repository
skeating Sep 3, 2025
036b1ec
Adds notes metadata audit repository
skeating Sep 4, 2025
8cae0a0
interchange classes
skeating Sep 16, 2025
51c37c9
hl7reader files
skeating Sep 16, 2025
e2ce0dd
core files that are not quite working yet
skeating Sep 16, 2025
14f5c26
Disables MRN and visit creation in NotesMetadata
skeating Sep 18, 2025
3d7e2cb
Adds notes metadata message processing
skeating Sep 19, 2025
eb5e61c
First working version of the test
skeating Sep 19, 2025
fc86a31
my first test Passes fully
skeating Sep 19, 2025
1edea8a
Refactors NotesMetadataFactory
skeating Sep 19, 2025
c185312
Uses message timestamp if note time is missing
skeating Sep 20, 2025
15ff2da
Parses notes metadata from MDM_T08 messages
skeating Sep 20, 2025
0503350
Adds NotesMetadataController and repository
skeating Sep 20, 2025
56f6953
fix check style
skeating Sep 20, 2025
fbeb02d
typo
skeating Sep 21, 2025
6857705
more check style
skeating Sep 21, 2025
614505c
Fixes typo in method parameter name
skeating Sep 21, 2025
88fc4f2
typo
skeating Sep 21, 2025
cf8c97b
Renames notesMetadataId to internalId
skeating Sep 21, 2025
1a254e4
Adds NotesMetadata processing
skeating Sep 21, 2025
9291ba4
fixed check style
skeating Sep 21, 2025
0659c7f
Pass linting
stefpiatek Sep 26, 2025
ff35e8a
Add lombok to annotation paths in emap interchange
stefpiatek Sep 26, 2025
a5c77a5
Update star annotations to run on m3 mac
stefpiatek Sep 26, 2025
472af4e
Add lastEditDatetime to minimal note message
stefpiatek Sep 26, 2025
f1165a3
Merge branch 'develop' into sarah/add_notes_metadata
stefpiatek Sep 26, 2025
9dfc6b2
Ensures notes metadata last edit datetime is populated
skeating Oct 11, 2025
96f473f
remove unused file
skeating Oct 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,13 @@
<target>${java.version}</target>
<release>${java.version}</release>
<forceJavacCompilerUse>true</forceJavacCompilerUse>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@
import uk.ac.ucl.rits.inform.datasinks.emapstar.dataprocessors.FlowsheetProcessor;
import uk.ac.ucl.rits.inform.datasinks.emapstar.dataprocessors.FormProcessor;
import uk.ac.ucl.rits.inform.datasinks.emapstar.dataprocessors.LabProcessor;
import uk.ac.ucl.rits.inform.datasinks.emapstar.dataprocessors.NotesMetadataProcessor;
import uk.ac.ucl.rits.inform.datasinks.emapstar.dataprocessors.PatientStateProcessor;
import uk.ac.ucl.rits.inform.datasinks.emapstar.dataprocessors.WaveformProcessor;
import uk.ac.ucl.rits.inform.interchange.AdvanceDecisionMessage;
import uk.ac.ucl.rits.inform.interchange.ConsultMetadata;
import uk.ac.ucl.rits.inform.interchange.ConsultRequest;
import uk.ac.ucl.rits.inform.interchange.EmapOperationMessageProcessingException;
import uk.ac.ucl.rits.inform.interchange.EmapOperationMessageProcessor;
import uk.ac.ucl.rits.inform.interchange.NotesMetadataMessage;
import uk.ac.ucl.rits.inform.interchange.PatientAllergy;
import uk.ac.ucl.rits.inform.interchange.PatientInfection;
import uk.ac.ucl.rits.inform.interchange.PatientProblem;
Expand Down Expand Up @@ -73,6 +75,8 @@ public class InformDbOperations implements EmapOperationMessageProcessor {
private FormProcessor formProcessor;
@Autowired
private WaveformProcessor waveformProcessor;
@Autowired
private NotesMetadataProcessor notesMetadataProcessor;

@Value("${features.sde:false}")
private boolean sdeFeatureEnabled;
Expand All @@ -83,6 +87,18 @@ public void logFeatureFlags() {
}


/**
* Process a notes metadata message.
* @param msg the message
* @throws EmapOperationMessageProcessingException if message could not be processed
*/
@Override
@Transactional
public void processMessage(NotesMetadataMessage msg) throws EmapOperationMessageProcessingException {
Instant storedFrom = Instant.now();
notesMetadataProcessor.processMessage(msg, storedFrom);
}

/**
* Process a lab order message.
* @param labOrderMsg the message
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package uk.ac.ucl.rits.inform.datasinks.emapstar.controllers;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import uk.ac.ucl.rits.inform.datasinks.emapstar.RowState;
import uk.ac.ucl.rits.inform.datasinks.emapstar.exceptions.MessageIgnoredException;
import uk.ac.ucl.rits.inform.datasinks.emapstar.repos.NotesMetadataAuditRepository;
import uk.ac.ucl.rits.inform.datasinks.emapstar.repos.NotesMetadataRepository;


import uk.ac.ucl.rits.inform.informdb.notes.NotesMetadata;
import uk.ac.ucl.rits.inform.informdb.notes.NotesMetadataAudit;
import uk.ac.ucl.rits.inform.informdb.identity.HospitalVisit;
import uk.ac.ucl.rits.inform.interchange.NotesMetadataMessage;

import java.time.Instant;

/**
* Controller for NotesMetadata specific information.
* @author Sarah Keating
*/
@Component
public class NotesMetadataController {
private final Logger logger = LoggerFactory.getLogger(getClass());

private final NotesMetadataRepository notesMetadataRepository;
private final NotesMetadataAuditRepository notesMetadataAuditRepository;

NotesMetadataController(
NotesMetadataRepository notesMetadataRepository,
NotesMetadataAuditRepository notesMetadataAuditRepository) {
this.notesMetadataRepository = notesMetadataRepository;
this.notesMetadataAuditRepository = notesMetadataAuditRepository;
}

/**
* Process notesMetadata data message.
* @param msg the interchange message
* @param visit the hospital visit
* @param storedFrom stored from timestamp
* @throws MessageIgnoredException if message not processed
*/
@Transactional
public void processMessage(
NotesMetadataMessage msg,
HospitalVisit visit,
Instant storedFrom) throws MessageIgnoredException {

RowState<NotesMetadata, NotesMetadataAudit> notesMetadataState = getOrCreateNotesMetadata(msg, visit, storedFrom);

if (messageShouldBeUpdated(msg.getLastEditDatetime(), notesMetadataState)) {
updateNotesMetadata(msg, notesMetadataState);
}

notesMetadataState.saveEntityOrAuditLogIfRequired(notesMetadataRepository, notesMetadataAuditRepository);
}

/**
* Get existing or create new advance decision.
* @param msg Advance decision message.
* @param visit Hospital visit of patient this advanced decision message refers to.
* @param storedFrom Time that emap-core started processing this advanced decision message.
* @return AdvancedDecision entity wrapped in RowState
*/
private RowState<NotesMetadata, NotesMetadataAudit> getOrCreateNotesMetadata(NotesMetadataMessage msg, HospitalVisit visit,
Instant storedFrom) {
return notesMetadataRepository
.findByInternalId(msg.getNotesMetadataId())
.map(obs -> new RowState<>(obs, msg.getLastEditDatetime(), storedFrom, false))
.orElseGet(() -> createMinimalNotesMetadata(msg, visit, storedFrom));
}

/**
* Create minimal advance decision wrapped in RowState.
* @param msg Advance decision message
* @param visit Hospital visit of the patient advanced decision was recorded for.
* @param storedFrom Time that emap-core started processing the advanced decision of that patient.
* @return minimal advanced decision wrapped in RowState
*/
private RowState<NotesMetadata, NotesMetadataAudit>
createMinimalNotesMetadata(NotesMetadataMessage msg,
HospitalVisit visit, Instant storedFrom) {
NotesMetadata notesMetadata = new NotesMetadata(
msg.getNotesMetadataId(), visit);
logger.debug("Created new {}", notesMetadata);
return new RowState<>(notesMetadata, msg.getLastEditDatetime(), storedFrom, true);
}

/**
* Decides whether or not the data held for a specific advance decision needs to be updated or not.
* @param statusChangeDatetime Datetime of NotesMetadataMessage that's currently processed.
* @param notesMetadataState State of advance decision created from message.
* @return true if message should be updated
*/
private boolean messageShouldBeUpdated(Instant statusChangeDatetime, RowState<NotesMetadata,
NotesMetadataAudit> notesMetadataState) {
return (notesMetadataState.isEntityCreated() || !statusChangeDatetime.isBefore(
notesMetadataState.getEntity().getValidFrom()));
}

/**
* Update advance decision data with information from NotesMetadataMessage.
* @param msg Advance decision message.
* @param notesMetadataState Advance decision referred to in message
*/
private void updateNotesMetadata(NotesMetadataMessage msg, RowState<NotesMetadata,
NotesMetadataAudit> notesMetadataState) {
NotesMetadata notesMetadata = notesMetadataState.getEntity();

notesMetadataState.assignIfDifferent(msg.getLastEditDatetime(), notesMetadata.getLastEditDatetime(),
notesMetadata::setLastEditDatetime);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package uk.ac.ucl.rits.inform.datasinks.emapstar.dataprocessors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import uk.ac.ucl.rits.inform.datasinks.emapstar.controllers.NotesMetadataController;
import uk.ac.ucl.rits.inform.datasinks.emapstar.controllers.PersonController;
import uk.ac.ucl.rits.inform.datasinks.emapstar.controllers.VisitController;

import uk.ac.ucl.rits.inform.informdb.identity.HospitalVisit;
import uk.ac.ucl.rits.inform.informdb.identity.Mrn;

import uk.ac.ucl.rits.inform.interchange.EmapOperationMessageProcessingException;
import uk.ac.ucl.rits.inform.interchange.NotesMetadataMessage;


import java.time.Instant;

/**
* Handle processing of notes metadata messages.
* @author Sarah Keating
*/
@Component
public class NotesMetadataProcessor {
private final Logger logger = LoggerFactory.getLogger(getClass());
private final PersonController personController;
private final VisitController visitController;
private final NotesMetadataController notesMetadataController;

/**
* Notes metadata controller to identify whether metadata needs to be updated; person controller to identify patient.
* @param notesMetadataController notes metadata controller
* @param personController person controller
* @param visitController hospital visit controller
*/
public NotesMetadataProcessor(NotesMetadataController notesMetadataController,
PersonController personController, VisitController visitController) {
this.personController = personController;
this.visitController = visitController;
this.notesMetadataController = notesMetadataController;
}

/**
* Process notes metadata message.
* @param msg message
* @param storedFrom Time the message started to be processed by star
* @throws EmapOperationMessageProcessingException if message can't be processed.
*/
@Transactional
public void processMessage(final NotesMetadataMessage msg, final Instant storedFrom)
throws EmapOperationMessageProcessingException {
Instant msgStatusChangeTime = msg.getLastEditDatetime();
if (msgStatusChangeTime == null) {
msgStatusChangeTime = storedFrom;
}

// retrieve patient to whom message refers to; if MRN not registered, create new patient
Mrn mrn = personController.getOrCreateOnMrnOnly(msg.getMrn(), null, msg.getSourceSystem(),
msgStatusChangeTime, storedFrom);
HospitalVisit visit = visitController.getOrCreateMinimalHospitalVisit(
msg.getVisitNumber(), mrn, msg.getSourceSystem(), msg.getLastEditDatetime(), storedFrom);
logger.trace("Processing {}", msg);
notesMetadataController.processMessage(msg, visit, storedFrom);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package uk.ac.ucl.rits.inform.datasinks.emapstar.repos;

import org.springframework.data.repository.CrudRepository;
import uk.ac.ucl.rits.inform.informdb.notes.NotesMetadataAudit;

import java.util.List;

/**
* Interaction with the NotesMetadataAudit table.
* @author Sarah Keating
*/
public interface NotesMetadataAuditRepository extends CrudRepository<NotesMetadataAudit, Long> {
/**
* For testing.
* @param hospitalVisitId id of the hospital visit
* @return all notes metadata audits
*/
List<NotesMetadataAudit> findAllByHospitalVisitId(long hospitalVisitId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package uk.ac.ucl.rits.inform.datasinks.emapstar.repos;

import org.springframework.data.repository.CrudRepository;

import uk.ac.ucl.rits.inform.informdb.identity.HospitalVisit;
import uk.ac.ucl.rits.inform.informdb.notes.NotesMetadata;

import java.util.List;
import java.util.Optional;

/**
* Interaction with the NotesMetadata table.
* @author Sarah Keating
*/
public interface NotesMetadataRepository extends CrudRepository<NotesMetadata, Long> {
/**
* Find notes metadata by unique identifier.
* @param internalId internal ID for the notes metadata
* @return possible NotesMetadata
*/
Optional<NotesMetadata> findByInternalId(Long internalId);

List<NotesMetadata> findAllByHospitalVisitId(HospitalVisit hospitalVisit);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package uk.ac.ucl.rits.inform.datasinks.emapstar;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import uk.ac.ucl.rits.inform.datasinks.emapstar.repos.HospitalVisitRepository;
import uk.ac.ucl.rits.inform.datasinks.emapstar.repos.MrnRepository;
import uk.ac.ucl.rits.inform.datasinks.emapstar.repos.NotesMetadataRepository;
import uk.ac.ucl.rits.inform.informdb.notes.NotesMetadata;
import uk.ac.ucl.rits.inform.informdb.identity.HospitalVisit;
import uk.ac.ucl.rits.inform.informdb.identity.Mrn;
import uk.ac.ucl.rits.inform.informdb.identity.MrnToLive;
import uk.ac.ucl.rits.inform.interchange.NotesMetadataMessage;
import uk.ac.ucl.rits.inform.interchange.EmapOperationMessageProcessingException;

import java.io.IOException;
import java.time.Instant;
import java.util.List;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;

/**
* Test cases to assert correct processing of NotesMetadataMessages.
* @author Anika Cawthorn
*/
public class TestNotesMetadataProcessing extends MessageProcessingBase {
@Autowired
NotesMetadataRepository notesMetadataRepo;
@Autowired
HospitalVisitRepository hospitalVisitRepository;
@Autowired
MrnRepository mrnRepository;

private NotesMetadataMessage minimal;
// private NotesMetadataMessage minimalWithQuestions;
// private NotesMetadataMessage closedAtDischarge;
// private NotesMetadataMessage cancelled;

private static Instant NOTES_METADATA_START_TIME = Instant.parse("2013-02-14T09:00:00Z");
private static Instant NOTES_METADATA_EDIT_TIME = Instant.parse("2013-02-14T09:00:00Z");
private static Long NOTES_METADATA_INTERNAL_ID = 1234521112L;
private static String NOTES_METADATA_MRN = "40800000";

@BeforeEach
public void setUp() throws IOException {
minimal = messageFactory.getNotesMetadataMessage("minimal.yaml");
}

/**
* Given that no MRNS or hospital visits exist in the database
* When a new NotesMetadataMessage without questions arrives
* Then a minimal HospitalVisit, Mrn and NotesMetadata should be created
*/
@Test
void testMinimalNotesMetadataCreated() throws EmapOperationMessageProcessingException {
processSingleMessage(minimal);

List<Mrn> mrns = getAllMrns();
assertEquals(1, mrns.size());

assertEquals(NOTES_METADATA_MRN, mrns.get(0).getMrn());
MrnToLive mrnToLive = mrnToLiveRepo.getByMrnIdEquals(mrns.get(0));
assertNotNull(mrnToLive);

Optional<HospitalVisit> visit = hospitalVisitRepository.findByEncounter(defaultEncounter);
assertTrue(visit.isPresent());



NotesMetadata notesMetadata = notesMetadataRepo.findByInternalId(NOTES_METADATA_INTERNAL_ID).orElseThrow();
assertEquals(NOTES_METADATA_EDIT_TIME, notesMetadata.getValidFrom());
assertEquals(NOTES_METADATA_START_TIME, notesMetadata.getStartedDatetime());

Check failure on line 74 in core/src/test/java/uk/ac/ucl/rits/inform/datasinks/emapstar/TestNotesMetadataProcessing.java

View workflow job for this annotation

GitHub Actions / JUnit Test Report

TestNotesMetadataProcessing.testMinimalNotesMetadataCreated

expected: <2013-02-14T09:00:00Z> but was: <null>
Raw output
org.opentest4j.AssertionFailedError: expected: <2013-02-14T09:00:00Z> but was: <null>
	at uk.ac.ucl.rits.inform.datasinks.emapstar.TestNotesMetadataProcessing.testMinimalNotesMetadataCreated(TestNotesMetadataProcessing.java:74)
}
}
5 changes: 4 additions & 1 deletion emap-checker.xml
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,10 @@
<!-- Checks for Size Violations. -->
<!-- See http://checkstyle.sf.net/config_sizes.html -->
<module name="MethodLength"/>
<module name="ParameterNumber"/>
<module name="ParameterNumber">
<property name="max" value="10"/>
<property name="tokens" value="METHOD_DEF"/>
</module>

<!-- Checks for whitespace -->
<!-- See http://checkstyle.sf.net/config_whitespace.html -->
Expand Down
7 changes: 7 additions & 0 deletions emap-interchange/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,13 @@
<source>${java.version}</source>
<target>${java.version}</target>
<forceJavacCompilerUse>true</forceJavacCompilerUse>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
Expand Down
Loading
Loading