Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,14 @@ void When_EhrStatusWithExceededTimeout_Expect_EhrStatusShouldNotBeUpdated() {
var inProgressConversationId = generateRandomUppercaseUUID();

var ehrExtractStatusServiceSpy = spy(ehrExtractStatusService);
when(timestampService.now()).thenReturn(NOW);

addInProgressTransferWithExceededAckTimeout(inProgressConversationId, List.of());

ehrExtractTimeoutScheduler.processEhrExtractAckTimeouts();
when(ehrExtractStatusServiceSpy.logger()).thenReturn(logger);


var ehrReceivedAcknowledgement = getEhrReceivedAcknowledgement(inProgressConversationId);
ehrReceivedAcknowledgement.setReceived(NOW);
ehrReceivedAcknowledgement.setConversationClosed(NOW);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,14 @@
@SpringBootTest(properties = {"command.line.runner.enabled=false"})
public class SendAcknowledgementComponentTest {
private static final String GENERATED_RANDOM_ID = "GENERATED-RANDOM-ID";
private static final String GENERATED_RANDOM_ID_2 = "GENERATED-RANDOM-ID-2";
private static final String FROM_ASID = "0000222-from-asid";
private static final String TO_ASID = "0000333-to-asid";
private static final String FROM_ODS_CODE = "0000222-from-ods-code";
private static final String TO_ODS_CODE = "0000333-to-ods-code";
private static final String EHR_REQUEST_MESSAGE_ID = "000-333-444-ehr-request-message-id";
private static final String DATE = "2018-03-04T03:10:41.01Z";
private static final String DATE_1 = "2018-03-04T03:10:41.01Z";
private static final String DATE_2 = "2018-03-04T03:10:42.01Z";
private static final String REASON_CODE = "06";
private static final String REASON_MESSAGE = "Patient not at surgery.";
private static final String TASK_ID = "999-000-task-id";
Expand Down Expand Up @@ -78,7 +80,7 @@ public class SendAcknowledgementComponentTest {
@BeforeEach
public void setUp() {
when(randomIdGeneratorService.createNewId()).thenReturn(GENERATED_RANDOM_ID);
when(timestampService.now()).thenReturn(Instant.parse(DATE));
when(timestampService.now()).thenReturn(Instant.parse(DATE_1));

ehrExtractStatus = EhrExtractStatusTestUtils.prepareEhrExtractStatus();
ehrExtractStatusRepository.save(ehrExtractStatus);
Expand All @@ -96,6 +98,20 @@ public void When_AcknowledgementTaskRunsTwice_Expect_DatabaseOverwritesEhrExtrac
when(sendAcknowledgementTaskDefinition.getFromOdsCode()).thenReturn(ehrRequest.getFromOdsCode());
when(mhsClient.sendMessageToMHS(request)).thenReturn("Successful Mhs Outbound Request");

when(randomIdGeneratorService.createNewId())
.thenReturn(GENERATED_RANDOM_ID)
.thenReturn(GENERATED_RANDOM_ID_2);

when(timestampService.now())
.thenReturn(Instant.parse(DATE_1))
.thenReturn(Instant.parse(DATE_1))
.thenReturn(Instant.parse(DATE_1))
.thenReturn(Instant.parse(DATE_1))
.thenReturn(Instant.parse(DATE_2))
.thenReturn(Instant.parse(DATE_2))
.thenReturn(Instant.parse(DATE_2))
.thenReturn(Instant.parse(DATE_2));

sendAcknowledgementExecutor.execute(sendAcknowledgementTaskDefinition);
var ehrExtractFirst = ehrExtractStatusRepository.findByConversationId(ehrExtractStatus.getConversationId()).get();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ public EhrExtractStatus updateEhrExtractStatusAccessStructured(GetGpcStructuredT
String structuredRecordJsonFilename) {

Query query = createQueryForConversationId(structuredTaskDefinition.getConversationId());
Instant now = Instant.now();
Instant now = timestampService.now();

Update update = createUpdateWithUpdatedAt();
update.set(STRUCTURE_ACCESSED_AT_PATH, now);
Expand Down Expand Up @@ -301,7 +301,7 @@ public Optional<EhrExtractStatus> updateEhrExtractStatusContinue(String conversa
Query query = createQueryForConversationId(conversationId);

Update update = createUpdateWithUpdatedAt();
Instant now = Instant.now();
Instant now = timestampService.now();
update.set(CONTINUE_RECEIVED_PATH, now);

FindAndModifyOptions returningUpdatedRecordOption = getReturningUpdatedRecordOption();
Expand Down Expand Up @@ -485,7 +485,7 @@ public EhrExtractStatus updateEhrExtractStatusError(
String taskType) {

Update update = createUpdateWithUpdatedAt();
Instant now = Instant.now();
Instant now = timestampService.now();
update.set(ERROR_OCCURRED_AT_PATH, now);
update.set(ERROR_CODE_PATH, errorCode);
update.set(ERROR_MESSAGE_PATH, errorMessage);
Expand Down Expand Up @@ -535,7 +535,7 @@ private EhrExtractStatus updateEhrExtractStatusDocumentSentToMHS(SendDocumentTas
var commonMessageId = GPC_DOCUMENTS + DOT + taskDefinition.getDocumentPosition() + DOT + SENT_TO_MHS + DOT + MESSAGE_ID;

Update update = createUpdateWithUpdatedAt();
update.set(commonSentAt, Instant.now());
update.set(commonSentAt, timestampService.now());
update.set(commonTaskId, taskDefinition.getTaskId());
update.set(commonMessageId, messageIds);

Expand All @@ -561,7 +561,7 @@ private EhrExtractStatus updateEhrExtractStatusAttachmentSentToMhs(SendDocumentT
var commonMessageId = STRUCTURE_OBJECT_AS_ATTACHMENT + DOT + SENT_TO_MHS + DOT + MESSAGE_ID;

Update update = createUpdateWithUpdatedAt();
update.set(commonSentAt, Instant.now());
update.set(commonSentAt, timestampService.now());
update.set(commonTaskId, taskDefinition.getTaskId());
update.set(commonMessageId, messageIds);

Expand Down Expand Up @@ -594,7 +594,7 @@ public Query createQueryForConversationId(String conversationId) {
}

public Update createUpdateWithUpdatedAt() {
Instant now = Instant.now();
Instant now = timestampService.now();
Update update = new Update();

update.set(UPDATED_AT, now);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,10 @@ public void execute(SendEhrExtractCoreTaskDefinition sendEhrExtractCoreTaskDefin

mhsClient.sendMessageToMHS(requestData);

Instant requestSentAtPending = Instant.now();
Instant requestSentAtPending = timestampService.now();
ehrExtractStatusService.updateEhrExtractStatusCorePending(sendEhrExtractCoreTaskDefinition, requestSentAtPending);

Instant requestSentAt = Instant.now();
Instant requestSentAt = timestampService.now();
var ehrExtractStatus = ehrExtractStatusService.updateEhrExtractStatusCore(sendEhrExtractCoreTaskDefinition, requestSentAt);
if (ehrExtractStatus.getGpcAccessDocument().getDocuments().isEmpty()) {
sendAcknowledgementTaskDispatcher.sendPositiveAcknowledgement(ehrExtractStatus);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import uk.nhs.adaptors.gp2gp.common.service.TimestampService;
import uk.nhs.adaptors.gp2gp.common.service.XPathService;
import uk.nhs.adaptors.gp2gp.ehr.EhrExtractStatusService;
import uk.nhs.adaptors.gp2gp.ehr.exception.EhrExtractException;
Expand All @@ -38,11 +39,12 @@ public class EhrExtractAckHandler {

private final XPathService xPathService;
private final EhrExtractStatusService ehrExtractStatusService;
private final TimestampService timestampService;

@SneakyThrows
public void handle(String conversationId, Document document) {
String ackTypeCode = xPathService.getNodeValue(document, ACK_TYPE_CODE_XPATH);
Instant now = Instant.now();
Instant now = timestampService.now();
String messageRef = xPathService.getNodeValue(document, MESSAGE_REF_XPATH);
String rootId = xPathService.getNodeValue(document, MESSAGE_ID_ROOT_XPATH);
EhrReceivedAcknowledgementBuilder ackBuilder = EhrReceivedAcknowledgement.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,11 @@
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import uk.nhs.adaptors.gp2gp.common.service.TimestampService;
import uk.nhs.adaptors.gp2gp.ehr.EhrExtractStatusService;
import uk.nhs.adaptors.gp2gp.ehr.exception.EhrExtractException;
import uk.nhs.adaptors.gp2gp.ehr.model.EhrExtractStatus;
import uk.nhs.adaptors.gp2gp.ehr.utils.ErrorDetail;

import java.time.Instant;
import java.util.List;
import java.util.Objects;

Expand All @@ -26,11 +25,12 @@ public class EhrExtractTimeoutScheduler {
private static final String ERROR = "error";
private final MongoTemplate mongoTemplate;
private final EhrExtractStatusService ehrExtractStatusService;
private final TimestampService timestampService;

@Scheduled(cron = "${timeout.cronTime}")
public void processEhrExtractAckTimeouts() {
List<EhrExtractStatus> inProgressEhrExtractTransfers = findInProgressTransfers();
var now = Instant.now();
var now = timestampService.now();
var ehrExtractStatusWithExceededUpdateLimit = inProgressEhrExtractTransfers
.stream()
.filter(ehrExtractStatus -> Objects.isNull(ehrExtractStatus.getEhrReceivedAcknowledgement())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -607,8 +607,77 @@ void shouldLogWarningWithMsgIgnoredWhenLateAcknowledgementReceivedAfter8DaysAndE
conversationId);
}

@Test
void shouldSetSentAtFromTimestampServiceWhenUpdatingDocumentSentToMHS() {
String conversationId = generateRandomUppercaseUUID();
String taskId = generateRandomUppercaseUUID();
int documentPosition = 0;
Instant fixedTimestamp = Instant.parse("2026-04-23T10:00:00Z");

SendDocumentTaskDefinition taskDefinition = SendDocumentTaskDefinition.builder()
.conversationId(conversationId)
.taskId(taskId)
.documentPosition(documentPosition)
.build();

when(timestampService.now()).thenReturn(fixedTimestamp);

EhrExtractStatus ehrExtractStatus = EhrExtractStatus.builder().build();
when(mongoTemplate.findAndModify(any(Query.class), any(Update.class), any(FindAndModifyOptions.class), eq(EhrExtractStatus.class)))
.thenReturn(ehrExtractStatus);

ehrExtractStatusService.updateEhrExtractStatusCommonForDocuments(taskDefinition, List.of("msg-id-1"));

verify(mongoTemplate).findAndModify(queryCaptor.capture(), updateCaptor.capture(),
any(FindAndModifyOptions.class), eq(EhrExtractStatus.class));

Document setDocument = (Document) updateCaptor.getValue().getUpdateObject().get("$set");
String sentAtPath = "gpcAccessDocument.documents." + documentPosition + ".sentToMhs.sentAt";
assertEquals(fixedTimestamp, setDocument.get(sentAtPath));
}

@Test
void shouldSetSentAtFromTimestampServiceWhenUpdatingAttachmentSentToMhs() {
String conversationId = generateRandomUppercaseUUID();
String taskId = generateRandomUppercaseUUID();
Instant updatedAtTimestamp = Instant.parse("2026-04-23T11:00:00Z");
Instant sentAtTimestamp = Instant.parse("2026-04-23T12:00:00Z");

SendDocumentTaskDefinition taskDefinition = SendDocumentTaskDefinition.builder()
.conversationId(conversationId)
.taskId(taskId)
.documentPosition(0)
.build();

when(timestampService.now())
.thenReturn(updatedAtTimestamp)
.thenReturn(sentAtTimestamp);

EhrExtractStatus ehrExtractStatus = EhrExtractStatus.builder().build();
when(mongoTemplate.findAndModify(any(Query.class), any(Update.class), any(FindAndModifyOptions.class), eq(EhrExtractStatus.class)))
.thenReturn(ehrExtractStatus);

ehrExtractStatusService.updateEhrExtractStatusCommonForExternalEhrExtract(taskDefinition, List.of("msg-id-1"));

verify(mongoTemplate).findAndModify(queryCaptor.capture(), updateCaptor.capture(),
any(FindAndModifyOptions.class), eq(EhrExtractStatus.class));

Document setDocument = (Document) updateCaptor.getValue().getUpdateObject().get("$set");

String sentAtPath = "gpcAccessStructured.attachment.sentToMhs.sentAt";
assertTrue(setDocument.containsKey(sentAtPath), "Update should contain the sentAt field");
assertEquals(sentAtTimestamp, setDocument.get(sentAtPath));

String taskIdPath = "gpcAccessStructured.attachment.sentToMhs.taskId";
assertEquals(taskId, setDocument.get(taskIdPath));

String messageIdPath = "gpcAccessStructured.attachment.sentToMhs.messageId";
assertEquals(List.of("msg-id-1"), setDocument.get(messageIdPath));
}

private String generateRandomUppercaseUUID() {
return UUID.randomUUID().toString().toUpperCase();
}

}
}

Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

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

import javax.xml.parsers.DocumentBuilderFactory;
Expand All @@ -32,6 +33,7 @@
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import uk.nhs.adaptors.gp2gp.common.service.TimestampService;
import uk.nhs.adaptors.gp2gp.common.service.XPathService;
import uk.nhs.adaptors.gp2gp.ehr.EhrExtractStatusService;
import uk.nhs.adaptors.gp2gp.ehr.exception.EhrExtractException;
Expand Down Expand Up @@ -66,6 +68,9 @@ class EhrExtractAckHandlerTest {
@Mock
private Document document;

@Mock
private TimestampService timestampService;

@Captor
private ArgumentCaptor<EhrExtractStatus.EhrReceivedAcknowledgement> receivedAckField;

Expand All @@ -90,6 +95,7 @@ void When_Handle_WithAckAndMessageRefEqualsEhrExtractMessageId_Expect_Conversati
when(xPathService.getNodeValue(any(), eq(ACK_TYPE_CODE_XPATH))).thenReturn(ACK_OK_CODE);
when(xPathService.getNodeValue(any(), eq(MESSAGE_REF_XPATH))).thenReturn(EHR_MESSAGE_REF);
when(ehrExtractStatusService.fetchEhrExtractMessageId(CONVERSATION_ID)).thenReturn(Optional.of(EHR_MESSAGE_REF));
when(timestampService.now()).thenReturn(Instant.now());

ehrExtractAckHandler.handle(CONVERSATION_ID, document);

Expand Down Expand Up @@ -141,6 +147,7 @@ void When_Handle_WithAckAndNoEhrExtractMessageIdForConversation_Expect_EhrExtrac

when(xPathService.getNodeValue(any(), eq(ACK_TYPE_CODE_XPATH))).thenReturn(ACK_OK_CODE);
when(xPathService.getNodeValue(any(), eq(MESSAGE_REF_XPATH))).thenReturn(EHR_MESSAGE_REF);
when(timestampService.now()).thenReturn(Instant.now());

when(ehrExtractStatusService.fetchEhrExtractMessageId(CONVERSATION_ID)).thenReturn(Optional.empty());

Expand All @@ -159,6 +166,7 @@ void When_Handle_WithNackReferencesEhrExtract_Expect_ConversationClosed() throws
when(xPathService.getNodeValue(any(), eq(MESSAGE_REF_XPATH))).thenReturn(EHR_MESSAGE_REF);
when(xPathService.getNodes(any(), eq(ERROR_CODE_XPATH))).thenReturn(codeNodeList);
when(ehrExtractStatusService.fetchEhrExtractMessageId(CONVERSATION_ID)).thenReturn(Optional.of(EHR_MESSAGE_REF));
when(timestampService.now()).thenReturn(Instant.now());

ehrExtractAckHandler.handle(CONVERSATION_ID, document);

Expand Down Expand Up @@ -196,6 +204,7 @@ void When_Handle_WithNackDoesNotReferenceExtract_Expect_ReceivedAckFieldNotUpdat
when(xPathService.getNodeValue(any(), eq(MESSAGE_REF_XPATH))).thenReturn(EHR_MESSAGE_REF);
when(xPathService.getNodes(any(), eq(ERROR_CODE_XPATH))).thenReturn(codeNodeList);
when(ehrExtractStatusService.fetchEhrExtractMessageId(CONVERSATION_ID)).thenReturn(Optional.of(RANDOM_MESSAGE_REF));
when(timestampService.now()).thenReturn(Instant.now());

ehrExtractAckHandler.handle(CONVERSATION_ID, document);

Expand All @@ -214,6 +223,7 @@ void When_Handle_WithNackDoesNotReferenceExtract_Expect_AckSaved() throws XPathE
when(xPathService.getNodeValue(any(), eq(MESSAGE_REF_XPATH))).thenReturn(EHR_MESSAGE_REF);
when(xPathService.getNodes(any(), eq(ERROR_CODE_XPATH))).thenReturn(codeNodeList);
when(ehrExtractStatusService.fetchEhrExtractMessageId(CONVERSATION_ID)).thenReturn(Optional.of(RANDOM_MESSAGE_REF));
when(timestampService.now()).thenReturn(Instant.now());

ehrExtractAckHandler.handle(CONVERSATION_ID, document);

Expand All @@ -232,6 +242,7 @@ void When_Handle_WithRejectedReferencesEhrExtract_Expect_ConversationClosed() th
when(xPathService.getNodeValue(any(), eq(MESSAGE_REF_XPATH))).thenReturn(EHR_MESSAGE_REF);
when(xPathService.getNodes(any(), eq(ACK_DETAILS_XPATH))).thenReturn(codeNodeList);
when(ehrExtractStatusService.fetchEhrExtractMessageId(CONVERSATION_ID)).thenReturn(Optional.of(EHR_MESSAGE_REF));
when(timestampService.now()).thenReturn(Instant.now());

ehrExtractAckHandler.handle(CONVERSATION_ID, document);

Expand Down Expand Up @@ -286,6 +297,7 @@ void When_Handle_WithRejectedDoesNotReferenceExtract_Expect_AckSaved() throws XP
when(xPathService.getNodeValue(any(), eq(MESSAGE_REF_XPATH))).thenReturn(EHR_MESSAGE_REF);
when(xPathService.getNodes(any(), eq(ACK_DETAILS_XPATH))).thenReturn(codeNodeList);
when(ehrExtractStatusService.fetchEhrExtractMessageId(CONVERSATION_ID)).thenReturn(Optional.of(RANDOM_MESSAGE_REF));
when(timestampService.now()).thenReturn(Instant.now());

ehrExtractAckHandler.handle(CONVERSATION_ID, document);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ class EhrExtractTimeoutSchedulerTest {
void setUp() {
ehrExtractStatusService = new EhrExtractStatusService(mongoTemplate, ehrExtractStatusRepository, timestampService);
ehrExtractStatusServiceSpy = spy(ehrExtractStatusService);
ehrExtractTimeoutScheduler = new EhrExtractTimeoutScheduler(mongoTemplate, ehrExtractStatusServiceSpy);
ehrExtractTimeoutScheduler = new EhrExtractTimeoutScheduler(mongoTemplate, ehrExtractStatusServiceSpy, timestampService);
}

@Test
Expand Down Expand Up @@ -285,6 +285,7 @@ void updateEhrExtractStatusListWithEhrReceivedAcknowledgementError() {

doReturn(List.of(ehrExtractStatus)).when(ehrExtractTimeoutSchedulerSpy).findInProgressTransfers();
when(ehrExtractTimeoutSchedulerSpy.logger()).thenReturn(logger);
when(timestampService.now()).thenReturn(Instant.now());

var exception = assertThrows(EhrExtractException.class, ehrExtractTimeoutSchedulerSpy::processEhrExtractAckTimeouts);

Expand Down Expand Up @@ -313,6 +314,7 @@ void shouldCatchExceptionIfUnexpectedConditionAriseWhileUpdatingEhrExtractStatus
var inProgressConversationId = generateRandomUppercaseUUID();
EhrExtractStatus ehrExtractStatus = addInProgressTransfers(inProgressConversationId);
doReturn(List.of(ehrExtractStatus)).when(ehrExtractTimeoutSchedulerSpy).findInProgressTransfers();
when(timestampService.now()).thenReturn(Instant.now());

Exception exception = new RuntimeException("Logger failure");
doThrow(exception).when(logger).info("Scheduler has started processing EhrExtract list with Ack timeouts");
Expand All @@ -338,6 +340,7 @@ void whenEhrExtractStatusIsNullInterceptExceptionAndLogErrorMsg() {
doReturn(null).when(mongoTemplate).findAndModify(any(Query.class), any(UpdateDefinition.class),
any(FindAndModifyOptions.class), any());
when(ehrExtractTimeoutSchedulerSpy.logger()).thenReturn(logger);
when(timestampService.now()).thenReturn(Instant.now());

var exception = assertThrows(EhrExtractException.class, ehrExtractTimeoutSchedulerSpy::processEhrExtractAckTimeouts);

Expand Down
Loading