Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
44c5df2
CCD-7712: CCD /validate MID callbacks are invoked before event author…
kiran-yenigala-hmcts May 15, 2026
e589a2a
CCD-7712: CCD /validate MID callbacks are invoked before event author…
kiran-yenigala-hmcts May 15, 2026
123eb69
CCD-7712: CCD /validate MID callbacks are invoked before event author…
kiran-yenigala-hmcts May 18, 2026
a68432d
CCD-7712: CCD /validate MID callbacks are invoked before event author…
kiran-yenigala-hmcts May 18, 2026
913c6b7
Merge branch 'master' into CCD-7712
kiran-yenigala-hmcts May 18, 2026
dc9ac2c
CCD-7712: Address Checkstyle issues
kiran-yenigala-hmcts May 18, 2026
1a4898e
Merge branch 'master' into CCD-7712
kiran-yenigala-hmcts May 18, 2026
7b43e9b
CCD-7712: fix smoke tests when event token is not valid
kiran-yenigala-hmcts May 18, 2026
a183914
CCD-7712: fix failing functional tests
kiran-yenigala-hmcts May 18, 2026
59b6d5a
CCD-7712: fix failing functional tests
kiran-yenigala-hmcts May 18, 2026
a15714d
CCD-7712: fix failing functional tests
kiran-yenigala-hmcts May 18, 2026
240d71e
Merge branch 'master' into CCD-7712
kiran-yenigala-hmcts May 18, 2026
a73bd39
CCD-7712: revert change in EventTokenService
kiran-yenigala-hmcts May 18, 2026
a26e055
CCD-7712: fix functional test failures
kiran-yenigala-hmcts May 18, 2026
3816760
CCD-7712: fix functional test failures
kiran-yenigala-hmcts May 18, 2026
d25388a
Merge branch 'master' into CCD-7712
kiran-yenigala-hmcts May 18, 2026
4e3cb18
CCD-7712: fix checkstyle issues
kiran-yenigala-hmcts May 18, 2026
0fb51ad
Merge branch 'master' into CCD-7712
kiran-yenigala-hmcts May 19, 2026
5a680ba
Merge branch 'master' into CCD-7712
kiran-yenigala-hmcts May 21, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import uk.gov.hmcts.ccd.domain.model.definition.WizardPage;
import uk.gov.hmcts.ccd.domain.model.std.CaseDataContent;
import uk.gov.hmcts.ccd.domain.model.std.Event;
import uk.gov.hmcts.ccd.domain.service.casedeletion.TimeToLiveService;
import uk.gov.hmcts.ccd.domain.service.common.CaseService;
import uk.gov.hmcts.ccd.domain.service.common.EventTriggerService;
import uk.gov.hmcts.ccd.domain.service.stdapi.CallbackInvoker;
Expand All @@ -35,19 +36,22 @@ public class MidEventCallback {
private final EventTriggerService eventTriggerService;
private final CaseDefinitionRepository caseDefinitionRepository;
private final CaseService caseService;
private final TimeToLiveService timeToLiveService;

@Autowired
public MidEventCallback(CallbackInvoker callbackInvoker,
UIDefinitionRepository uiDefinitionRepository,
EventTriggerService eventTriggerService,
@Qualifier(CachedCaseDefinitionRepository.QUALIFIER)
CaseDefinitionRepository caseDefinitionRepository,
CaseService caseService) {
CaseDefinitionRepository caseDefinitionRepository,
CaseService caseService,
TimeToLiveService timeToLiveService) {
this.callbackInvoker = callbackInvoker;
this.uiDefinitionRepository = uiDefinitionRepository;
this.eventTriggerService = eventTriggerService;
this.caseDefinitionRepository = caseDefinitionRepository;
this.caseService = caseService;
this.timeToLiveService = timeToLiveService;
}

@Transactional
Expand Down Expand Up @@ -85,6 +89,7 @@ public Map<String, JsonNode> invoke(String caseTypeId, CaseDataContent content,
}
removeNextPageFieldData(currentOrNewCaseDetails, wizardPageOptional.get().getOrder(), caseTypeId,
event.getEventId());
applyTtlIncrementIfConfigured(currentOrNewCaseDetails, caseEventDefinition, caseTypeDefinition);

CaseDetails caseDetailsFromMidEventCallback =
callbackInvoker.invokeMidEventCallback(wizardPageOptional.get(),
Expand Down Expand Up @@ -130,4 +135,13 @@ private CaseEventDefinition getCaseEvent(Event event, CaseTypeDefinition caseTyp
}
return caseEventDefinition;
}

private void applyTtlIncrementIfConfigured(CaseDetails caseDetails,
CaseEventDefinition caseEventDefinition,
CaseTypeDefinition caseTypeDefinition) {
if (timeToLiveService.isCaseTypeUsingTTL(caseTypeDefinition)) {
caseDetails.setData(timeToLiveService.updateCaseDetailsWithTTL(
caseDetails.getData(), caseEventDefinition, caseTypeDefinition));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,35 @@
import org.springframework.stereotype.Service;
import uk.gov.hmcts.ccd.ApplicationParams;
import uk.gov.hmcts.ccd.config.JacksonUtils;
import uk.gov.hmcts.ccd.data.casedetails.CachedCaseDetailsRepository;
import uk.gov.hmcts.ccd.data.casedetails.CaseDetailsRepository;
import uk.gov.hmcts.ccd.data.definition.CachedCaseDefinitionRepository;
import uk.gov.hmcts.ccd.data.definition.CaseDefinitionRepository;
import uk.gov.hmcts.ccd.domain.model.callbacks.EventTokenProperties;
import uk.gov.hmcts.ccd.domain.model.casedataaccesscontrol.AccessProfile;
import uk.gov.hmcts.ccd.domain.model.definition.CaseDetails;
import uk.gov.hmcts.ccd.domain.model.definition.CaseTypeDefinition;
import uk.gov.hmcts.ccd.domain.model.std.CaseDataContent;
import uk.gov.hmcts.ccd.domain.model.std.Event;
import uk.gov.hmcts.ccd.domain.service.callbacks.EventTokenService;
import uk.gov.hmcts.ccd.domain.service.common.AccessControlService;
import uk.gov.hmcts.ccd.domain.service.common.CaseAccessService;
import uk.gov.hmcts.ccd.domain.service.common.ConditionalFieldRestorer;
import uk.gov.hmcts.ccd.domain.service.createevent.MidEventCallback;
import uk.gov.hmcts.ccd.domain.service.getcase.GetCaseOperation;
import uk.gov.hmcts.ccd.endpoint.exceptions.ResourceNotFoundException;
import uk.gov.hmcts.ccd.endpoint.exceptions.ValidationException;

import java.util.Map;
import java.util.Set;

import static com.google.common.collect.Maps.newHashMap;
import static uk.gov.hmcts.ccd.domain.service.common.AccessControlService.CAN_CREATE;
import static uk.gov.hmcts.ccd.domain.service.common.AccessControlService.CAN_READ;
import static uk.gov.hmcts.ccd.domain.service.common.AccessControlService.CAN_UPDATE;
import static uk.gov.hmcts.ccd.domain.service.common.AccessControlService.NO_CASE_STATE_FOUND;
import static uk.gov.hmcts.ccd.domain.service.common.AccessControlService.NO_CASE_TYPE_FOUND;
import static uk.gov.hmcts.ccd.domain.service.common.AccessControlService.NO_EVENT_FOUND;

@Service
@Slf4j
Expand All @@ -37,6 +50,9 @@ public class AuthorisedValidateCaseFieldsOperation implements ValidateCaseFields
private final ConditionalFieldRestorer conditionalFieldRestorer;
private final ApplicationParams applicationParams;
private final MidEventCallback midEventCallback;
private final GetCaseOperation getCaseOperation;
private final EventTokenService eventTokenService;
private final CaseDetailsRepository caseDetailsRepository;

public AuthorisedValidateCaseFieldsOperation(AccessControlService accessControlService,
@Qualifier(CachedCaseDefinitionRepository.QUALIFIER)
Expand All @@ -46,14 +62,21 @@ public AuthorisedValidateCaseFieldsOperation(AccessControlService accessControlS
ValidateCaseFieldsOperation validateCaseFieldsOperation,
ConditionalFieldRestorer conditionalFieldRestorer,
ApplicationParams applicationParams,
MidEventCallback midEventCallback) {
MidEventCallback midEventCallback,
@Qualifier("default") GetCaseOperation getCaseOperation,
EventTokenService eventTokenService,
@Qualifier(CachedCaseDetailsRepository.QUALIFIER)
CaseDetailsRepository caseDetailsRepository) {
this.accessControlService = accessControlService;
this.caseDefinitionRepository = caseDefinitionRepository;
this.caseAccessService = caseAccessService;
this.validateCaseFieldsOperation = validateCaseFieldsOperation;
this.conditionalFieldRestorer = conditionalFieldRestorer;
this.applicationParams = applicationParams;
this.midEventCallback = midEventCallback;
this.getCaseOperation = getCaseOperation;
this.eventTokenService = eventTokenService;
this.caseDetailsRepository = caseDetailsRepository;
}

@Override
Expand All @@ -64,8 +87,18 @@ public Map<String, JsonNode> validateCaseDetails(OperationContext operationConte
String caseTypeId = operationContext.caseTypeId();
String pageId = operationContext.pageId();

resolveCaseReferenceFromEventToken(content);

if (StringUtils.isNotBlank(pageId)) {
verifyEventIsPresent(content);
}

callMidEventCallback(caseTypeId, content, pageId);

if (StringUtils.isNotBlank(pageId)) {
verifyEventAccessAfterMidEvent(operationContext, content);
}

if (applicationParams.getExcludeVerifyAccessCaseTypesForValidate()
.stream()
.anyMatch(c -> c.equalsIgnoreCase(caseTypeId))) {
Expand All @@ -90,6 +123,140 @@ public Map<String, JsonNode> validateCaseDetails(OperationContext operationConte
return content.getData();
}

private void verifyEventIsPresent(CaseDataContent content) {
Event event = content.getEvent();
if (event == null || StringUtils.isEmpty(event.getEventId())) {
throw new ResourceNotFoundException(NO_EVENT_FOUND);
}
}

private void verifyEventAccessAfterMidEvent(OperationContext operationContext, CaseDataContent content) {
String caseTypeId = operationContext.caseTypeId();

final CaseTypeDefinition caseTypeDefinition = getCaseDefinitionType(caseTypeId);

if (StringUtils.isEmpty(content.getCaseReference())) {
if (hasUnresolvedCaseIdInEventToken(content)) {
throw new ResourceNotFoundException("Cannot find matching start trigger");
}
verifyCreateCaseEventAccess(content, caseTypeDefinition);
} else {
verifyUpdateCaseEventAccess(content);
}
}

private boolean hasUnresolvedCaseIdInEventToken(CaseDataContent content) {
if (StringUtils.isEmpty(content.getToken()) || StringUtils.isNotEmpty(content.getCaseReference())) {
return false;
}
try {
EventTokenProperties eventTokenProperties = eventTokenService.parseToken(content.getToken());
return StringUtils.isNotEmpty(eventTokenProperties.getCaseId());
} catch (RuntimeException e) {
log.debug("Unable to determine case id from event token: {}", e.getMessage());
return false;
}
}

private void resolveCaseReferenceFromEventToken(CaseDataContent content) {
if (StringUtils.isNotEmpty(content.getCaseReference()) || StringUtils.isEmpty(content.getToken())) {
return;
}
try {
EventTokenProperties eventTokenProperties = eventTokenService.parseToken(content.getToken());
if (StringUtils.isNotEmpty(eventTokenProperties.getCaseId())) {
content.setCaseReference(toCaseReference(eventTokenProperties.getCaseId()));
}
} catch (RuntimeException e) {
log.debug("Unable to resolve case reference from event token: {}", e.getMessage());
}
}

private String toCaseReference(String caseIdFromToken) {
if (StringUtils.isEmpty(caseIdFromToken)) {
return caseIdFromToken;
}
try {
if (getCaseOperation.execute(caseIdFromToken).isPresent()) {
return caseIdFromToken;
}
} catch (RuntimeException e) {
log.debug("Unable to load case by reference from event token: {}", e.getMessage());
}
try {
CaseDetails caseDetails = caseDetailsRepository.findById(Long.valueOf(caseIdFromToken));
if (caseDetails != null && StringUtils.isNotEmpty(caseDetails.getReferenceAsString())) {
return caseDetails.getReferenceAsString();
}
} catch (NumberFormatException e) {
log.debug("Case id from event token is not a numeric entity id: {}", caseIdFromToken);
} catch (RuntimeException e) {
log.debug("Unable to load case by entity id from event token: {}", e.getMessage());
}
return caseIdFromToken;
}

private void verifyCreateCaseEventAccess(CaseDataContent content, CaseTypeDefinition caseTypeDefinition) {
Set<AccessProfile> userRoles = caseAccessService.getCaseCreationRoles(caseTypeDefinition.getId());
if (userRoles == null || userRoles.isEmpty()) {
throw new ValidationException("Cannot find user roles for the user");
}
if (!accessControlService.canAccessCaseTypeWithCriteria(
caseTypeDefinition,
userRoles,
CAN_CREATE)) {
throw new ResourceNotFoundException(NO_CASE_TYPE_FOUND);
}
if (!accessControlService.canAccessCaseEventWithCriteria(
content.getEvent().getEventId(),
caseTypeDefinition.getEvents(),
userRoles,
CAN_CREATE)) {
throw new ResourceNotFoundException(NO_EVENT_FOUND);
}
}

private void verifyUpdateCaseEventAccess(CaseDataContent content) {
String caseReference = content.getCaseReference();
CaseDetails existingCaseDetails = getCaseOperation.execute(caseReference)
.orElseThrow(() -> new ResourceNotFoundException("Case not found"));

final CaseTypeDefinition caseTypeDefinition =
getCaseDefinitionType(existingCaseDetails.getCaseTypeId());

String caseReferenceForAccess = existingCaseDetails.getReferenceAsString();
Set<AccessProfile> accessProfiles =
caseAccessService.getAccessProfilesByCaseReference(caseReferenceForAccess);
if (accessProfiles == null || accessProfiles.isEmpty()) {
throw new ValidationException("Cannot find user roles for the user");
}

verifyCaseTypeAndStateAccessForUpdate(existingCaseDetails, caseTypeDefinition, accessProfiles);

if (!accessControlService.canAccessCaseEventWithCriteria(
content.getEvent().getEventId(),
caseTypeDefinition.getEvents(),
accessProfiles,
CAN_CREATE)) {
throw new ResourceNotFoundException(NO_EVENT_FOUND);
}
}

private void verifyCaseTypeAndStateAccessForUpdate(CaseDetails existingCaseDetails,
CaseTypeDefinition caseTypeDefinition,
Set<AccessProfile> accessProfiles) {
if (!accessControlService.canAccessCaseTypeWithCriteria(caseTypeDefinition, accessProfiles, CAN_UPDATE)) {
throw new ResourceNotFoundException(NO_CASE_TYPE_FOUND);
}
if (!accessControlService.canAccessCaseStateWithCriteria(
existingCaseDetails.getState(),
caseTypeDefinition,
accessProfiles,
CAN_UPDATE)) {
throw new ResourceNotFoundException(NO_CASE_STATE_FOUND);
}
}

private void callMidEventCallback(String caseTypeId, CaseDataContent content, String pageId) {
content.setData(midEventCallback.invoke(caseTypeId, content, pageId));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import uk.gov.hmcts.ccd.domain.model.definition.WizardPageField;
import uk.gov.hmcts.ccd.domain.model.std.CaseDataContent;
import uk.gov.hmcts.ccd.domain.model.std.Event;
import uk.gov.hmcts.ccd.domain.service.casedeletion.TimeToLiveService;
import uk.gov.hmcts.ccd.domain.service.common.CaseService;
import uk.gov.hmcts.ccd.domain.service.common.EventTriggerService;
import uk.gov.hmcts.ccd.domain.service.stdapi.CallbackInvoker;
Expand Down Expand Up @@ -65,6 +66,8 @@ class MidEventCallbackTest {
private UIDefinitionRepository uiDefinitionRepository;
@Mock
private CaseService caseService;
@Mock
private TimeToLiveService timeToLiveService;

@Mock
private CaseEventDefinition caseEventDefinition;
Expand Down Expand Up @@ -93,6 +96,7 @@ void setUp() {
WizardPage wizardPageWithoutCallback = createWizardPage("createCase2");
given(uiDefinitionRepository.getWizardPageCollection(CASE_TYPE_ID, event.getEventId()))
.willReturn(asList(wizardPageWithCallback, wizardPageWithoutCallback));
given(timeToLiveService.isCaseTypeUsingTTL(caseTypeDefinition)).willReturn(false);
}

@Test
Expand Down Expand Up @@ -439,6 +443,40 @@ void shouldFilterCaseDataContentWhenWizardPageOrderExists() {
IGNORE_WARNINGS);
}

@Test
@DisplayName("should apply TTL increment before invoking mid event callback for an existing case")
void shouldApplyTtlIncrementBeforeInvokingMidEventCallbackForExistingCase() {
Map<String, JsonNode> populatedData = new HashMap<>(data);
populatedData.put("SystemTTLField", new TextNode("2036-05-18"));
CaseDetails existingCaseDetails = caseDetails(data);
CaseDetails populatedCaseDetails = caseDetails(populatedData);

CaseDataContent content = newCaseDataContent()
.withEvent(event)
.withData(data)
.withCaseReference(CASE_REFERENCE)
.withIgnoreWarning(IGNORE_WARNINGS)
.build();

given(caseService.getCaseDetails(JURISDICTION_ID, CASE_REFERENCE)).willReturn(existingCaseDetails);
given(caseService.clone(existingCaseDetails)).willReturn(existingCaseDetails);
given(caseService.populateCurrentCaseDetailsWithEventFields(content, existingCaseDetails))
.willReturn(populatedCaseDetails);
given(timeToLiveService.isCaseTypeUsingTTL(caseTypeDefinition)).willReturn(true);
given(timeToLiveService.updateCaseDetailsWithTTL(populatedData, caseEventDefinition, caseTypeDefinition))
.willReturn(populatedData);
given(callbackInvoker.invokeMidEventCallback(wizardPageWithCallback,
caseTypeDefinition,
caseEventDefinition,
existingCaseDetails,
populatedCaseDetails,
IGNORE_WARNINGS)).willReturn(populatedCaseDetails);

midEventCallback.invoke(CASE_TYPE_ID, content, "createCase1");

verify(timeToLiveService).updateCaseDetailsWithTTL(populatedData, caseEventDefinition, caseTypeDefinition);
}

private Map<String, JsonNode> createData() {
Map<String, JsonNode> data = new HashMap<>();
data.put("createCase2_field1", new TextNode("test1"));
Expand Down
Loading