Repository: govuk-one-login/ipv-core-back
Pull request #3031: PYIC-7982: add support for checkMitigation in journey map
Author: login: thebauSoftwire
Created: 2025-03-10T17:39:23Z | Merged: 2025-03-13T11:48:18Z

<!-- Provide a general summary of your changes in the Title above -->
<!-- Include the Jira ticket number in square brackets as prefix, eg `[P4-XXXX] PR Title` -->

## Proposed changes

### What changed
- Updates process-journey-event to handle new `checkMitigation` on events

| mitigation on the journey map |
|-|
| <img width="1371" alt="Screenshot 2025-03-10 at 21 14 12" src="https://github.com/user-attachments/assets/66457bb2-52e4-45f6-829e-07a0c8eefc3a" /> |


### Why did it change
This will help de-duplicate states in our journey map which specifically handle mitigations as well as let us handle mitigations from any state type

### Issue tracking
<!-- List any related Jira tickets or GitHub issues -->
<!-- List any related ADRs or RFCs -->
<!-- Delete/copy as appropriate -->

- [PYIC-7982](https://govukverify.atlassian.net/browse/PYIC-7982)


[PYIC-7982]: https://govukverify.atlassian.net/browse/PYIC-7982?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ


Diff:
diff --git a/journey-map/public/helpers.mjs b/journey-map/public/helpers.mjs
index bd041fe2b2..69a6af9e40 100644
--- a/journey-map/public/helpers.mjs
+++ b/journey-map/public/helpers.mjs
@@ -81,6 +81,14 @@ export const resolveEventTargets = (
     },
   );
 
+  Object.keys(definition.checkMitigation || {}).forEach((mitigation) => {
+    const resolved = definition.checkMitigation[mitigation];
+    resolved.mitigation = mitigation;
+
+    const targets = resolveEventTargets(resolved, formData);
+    resolvedTargets.push(...targets);
+  });
+
   // Look for an override for feature flags
   const featureFlags = formData.getAll("featureFlag");
   const featureFlagResolution = Object.keys(
diff --git a/journey-map/public/render.mjs b/journey-map/public/render.mjs
index 537ccf1a6a..64ce328545 100644
--- a/journey-map/public/render.mjs
+++ b/journey-map/public/render.mjs
@@ -4,6 +4,9 @@ const topDownJourneys = ["INITIAL_JOURNEY_SELECTION"];
 const errorJourneys = ["TECHNICAL_ERROR"];
 const failureJourneys = ["INELIGIBLE", "FAILED"];
 
+const JOURNEY_CONTEXT_TRANSITION_CLASSNAME = "journeyCtxTransition";
+const MITIGATIONS_TRANSITION_CLASSNAME = "mitigationTransition";
+
 // Expand out parent states
 // Will also search 'otherStates' (e.g. for nested journeys)
 const expandParents = (journeyStates, otherStates) => {
@@ -162,6 +165,7 @@ const renderTransitions = (journeyStates, formData) => {
           targetEntryEvent,
           exitEventToEmit,
           journeyContext,
+          mitigation,
         } = resolvedTarget;
 
         const target = exitEventToEmit
@@ -188,11 +192,13 @@ const renderTransitions = (journeyStates, formData) => {
         }
 
         eventsByTarget[target] = eventsByTarget[target] || [];
-        const eventTransitionLabel = `${eventName}${targetEntryEvent ? `/${targetEntryEvent}` : ""}${journeyContext ? ` - journeyContext: ${journeyContext}` : ""}`;
         eventsByTarget[target].push(
-          journeyContext
-            ? `<span class="journeyCtxTransition">${eventTransitionLabel}</span>`
-            : eventTransitionLabel,
+          createTransitionLabel({
+            eventName,
+            targetEntryEvent,
+            journeyContext,
+            mitigation,
+          }),
         );
 
         if (!journeyStates[target]) {
@@ -225,6 +231,36 @@ const renderTransitions = (journeyStates, formData) => {
   return { transitionsMermaid: stateTransitions.join("\n"), states };
 };
 
+const createTransitionLabel = ({
+  eventName,
+  targetEntryEvent,
+  journeyContext,
+  mitigation,
+}) => {
+  const eventLabel = `${eventName}${targetEntryEvent ? `/${targetEntryEvent}` : ""}`;
+
+  const labelWithClass = (className, label, value) =>
+    `<span class="${className}">${eventLabel} - ${label}: ${value}</span>`;
+
+  if (journeyContext) {
+    return labelWithClass(
+      JOURNEY_CONTEXT_TRANSITION_CLASSNAME,
+      "journeyContext",
+      journeyContext,
+    );
+  }
+
+  if (mitigation) {
+    return labelWithClass(
+      MITIGATIONS_TRANSITION_CLASSNAME,
+      "mitigation",
+      mitigation,
+    );
+  }
+
+  return eventLabel;
+};
+
 const renderClickHandler = (state, definition) => {
   if (definition.nestedJourney) {
     definition.response = {
diff --git a/journey-map/public/style.css b/journey-map/public/style.css
index f6a8a05657..df88bdb5f6 100644
--- a/journey-map/public/style.css
+++ b/journey-map/public/style.css
@@ -224,3 +224,6 @@ form fieldset p {
 .journeyCtxList {
   padding: 4px 24px 0;
 }
+.mitigationTransition {
+  background-color: #e4c4ff;
+}
diff --git a/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/ProcessJourneyEventHandler.java b/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/ProcessJourneyEventHandler.java
index d9af5da940..3c13c570e1 100644
--- a/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/ProcessJourneyEventHandler.java
+++ b/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/ProcessJourneyEventHandler.java
@@ -36,6 +36,7 @@
 import uk.gov.di.ipv.core.library.persistence.item.ClientOAuthSessionItem;
 import uk.gov.di.ipv.core.library.persistence.item.IpvSessionItem;
 import uk.gov.di.ipv.core.library.service.AuditService;
+import uk.gov.di.ipv.core.library.service.CimitUtilityService;
 import uk.gov.di.ipv.core.library.service.ClientOAuthSessionDetailsService;
 import uk.gov.di.ipv.core.library.service.ConfigService;
 import uk.gov.di.ipv.core.library.service.IpvSessionService;
@@ -44,13 +45,14 @@
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.StateMachine;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.StateMachineInitializer;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.StateMachineInitializerMode;
+import uk.gov.di.ipv.core.processjourneyevent.statemachine.events.EventResolveParameters;
+import uk.gov.di.ipv.core.processjourneyevent.statemachine.events.EventResolver;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.exceptions.StateMachineNotFoundException;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.exceptions.UnknownEventException;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.exceptions.UnknownStateException;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.states.BasicState;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.states.JourneyChangeState;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.states.State;
-import uk.gov.di.ipv.core.processjourneyevent.statemachine.stepresponses.JourneyContext;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.stepresponses.PageStepResponse;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.stepresponses.ProcessStepResponse;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.stepresponses.StepResponse;
@@ -91,6 +93,7 @@ public class ProcessJourneyEventHandler
     private final ClientOAuthSessionDetailsService clientOAuthSessionService;
     private final Map<IpvJourneyTypes, StateMachine> stateMachines;
     private final EvcsService evcsService;
+    private final CimitUtilityService cimitUtilityService;
 
     @SuppressWarnings("java:S107") // Methods should not have too many parameters
     public ProcessJourneyEventHandler(
@@ -101,7 +104,8 @@ public ProcessJourneyEventHandler(
             List<IpvJourneyTypes> journeyTypes,
             StateMachineInitializerMode stateMachineInitializerMode,
             List<String> nestedJourneyTypes,
-            EvcsService evcsService)
+            EvcsService evcsService,
+            CimitUtilityService cimitUtilityService)
             throws IOException {
         this.ipvSessionService = ipvSessionService;
         this.auditService = auditService;
@@ -110,6 +114,7 @@ public ProcessJourneyEventHandler(
         this.stateMachines =
                 loadStateMachines(journeyTypes, stateMachineInitializerMode, nestedJourneyTypes);
         this.evcsService = evcsService;
+        this.cimitUtilityService = cimitUtilityService;
     }
 
     @ExcludeFromGeneratedCoverageReport
@@ -135,6 +140,7 @@ public ProcessJourneyEventHandler(ConfigService configService) throws IOExceptio
                         StateMachineInitializerMode.STANDARD,
                         nestedJourneyTypes);
         this.evcsService = new EvcsService(configService);
+        this.cimitUtilityService = new CimitUtilityService(configService);
     }
 
     @Override
@@ -268,31 +274,34 @@ private StepResponse executeJourneyEvent(
 
             return ((BasicState) newState).getResponse();
         } catch (UnknownStateException e) {
-            LOGGER.error(
-                    LogHelper.buildErrorMessage(
-                                    "Invalid journey state encountered, failed to execute journey engine step.",
-                                    e)
-                            .with(
-                                    LOG_USER_STATE.getFieldName(),
-                                    ipvSessionItem.getState().state()));
+            logErrorWithCurrentJourneyDetails(
+                    "Invalid journey state encountered, failed to execute journey engine step.",
+                    e,
+                    journeyEvent,
+                    ipvSessionItem.getState());
             throw new JourneyEngineException();
         } catch (UnknownEventException e) {
-            LOGGER.error(
-                    LogHelper.buildErrorMessage(
-                                    "Invalid journey event provided, failed to execute journey engine step.",
-                                    e)
-                            .with(LOG_JOURNEY_EVENT.getFieldName(), journeyEvent));
+            logErrorWithCurrentJourneyDetails(
+                    "Invalid journey event provided, failed to execute journey engine step.",
+                    e,
+                    journeyEvent,
+                    ipvSessionItem.getState());
             throw new JourneyEngineException();
         } catch (StateMachineNotFoundException e) {
-            LOGGER.error(
-                    LogHelper.buildErrorMessage(
-                                    "State machine not found for journey type, failed to execute journey engine step",
-                                    e)
-                            .with(LOG_JOURNEY_EVENT.getFieldName(), journeyEvent)
-                            .with(
-                                    LOG_JOURNEY_TYPE.getFieldName(),
-                                    ipvSessionItem.getState().subJourney().name()));
+            logErrorWithCurrentJourneyDetails(
+                    "State machine not found for journey type, failed to execute journey engine step",
+                    e,
+                    journeyEvent,
+                    ipvSessionItem.getState());
             throw new JourneyEngineException();
+        } catch (CredentialParseException e) {
+            logErrorWithCurrentJourneyDetails(
+                    "Unable to parse credentials.", e, journeyEvent, ipvSessionItem.getState());
+            throw new JourneyEngineException();
+        } catch (JourneyEngineException e) {
+            logErrorWithCurrentJourneyDetails(
+                    e.getMessage(), e, journeyEvent, ipvSessionItem.getState());
+            throw e;
         }
     }
 
@@ -306,7 +315,7 @@ private State executeStateTransition(
             String deviceInformation,
             ClientOAuthSessionItem clientOAuthSessionItem)
             throws StateMachineNotFoundException, UnknownEventException, UnknownStateException,
-                    EvcsServiceException, CredentialParseException {
+                    JourneyEngineException, EvcsServiceException, CredentialParseException {
 
         StateMachine stateMachine = stateMachines.get(initialJourneyState.subJourney());
         if (stateMachine == null) {
@@ -325,12 +334,18 @@ private State executeStateTransition(
             return handleBackEvent(ipvSessionItem, initialJourneyState);
         }
 
+        var eventResolver = new EventResolver(cimitUtilityService, configService);
+
         var result =
                 stateMachine.transition(
                         initialJourneyState.state(),
                         journeyEvent,
-                        new JourneyContext(configService, ipvSessionItem.getJourneyContext()),
-                        currentPage);
+                        currentPage,
+                        new EventResolveParameters(
+                                ipvSessionItem.getJourneyContext(),
+                                ipvSessionItem,
+                                clientOAuthSessionItem),
+                        eventResolver);
 
         if (!isNullOrEmpty(result.auditEvents())) {
             for (var auditEventType : result.auditEvents()) {
@@ -545,4 +560,13 @@ private boolean isBackEventDefinedOnState(JourneyState journeyState) {
                         instanceof BasicState basicState
                 && basicState.getEvents().containsKey(BACK_EVENT);
     }
+
+    private void logErrorWithCurrentJourneyDetails(
+            String message, Exception e, String journeyEvent, JourneyState journeyState) {
+        LOGGER.error(
+                LogHelper.buildErrorMessage(message, e)
+                        .with(LOG_JOURNEY_EVENT.getFieldName(), journeyEvent)
+                        .with(LOG_USER_STATE.getFieldName(), journeyState.state())
+                        .with(LOG_JOURNEY_TYPE.getFieldName(), journeyState.subJourney().name()));
+    }
 }
diff --git a/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/exceptions/JourneyEngineException.java b/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/exceptions/JourneyEngineException.java
index 5a582fd089..9c567e56ca 100644
--- a/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/exceptions/JourneyEngineException.java
+++ b/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/exceptions/JourneyEngineException.java
@@ -1,3 +1,15 @@
 package uk.gov.di.ipv.core.processjourneyevent.exceptions;
 
-public class JourneyEngineException extends Exception {}
+public class JourneyEngineException extends Exception {
+    public JourneyEngineException() {
+        super();
+    }
+
+    public JourneyEngineException(String message) {
+        super(message);
+    }
+
+    public JourneyEngineException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/StateMachine.java b/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/StateMachine.java
index e5b03b97d8..94828f74c9 100644
--- a/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/StateMachine.java
+++ b/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/StateMachine.java
@@ -1,13 +1,15 @@
 package uk.gov.di.ipv.core.processjourneyevent.statemachine;
 
 import uk.gov.di.ipv.core.library.domain.JourneyState;
+import uk.gov.di.ipv.core.processjourneyevent.exceptions.JourneyEngineException;
+import uk.gov.di.ipv.core.processjourneyevent.statemachine.events.EventResolveParameters;
+import uk.gov.di.ipv.core.processjourneyevent.statemachine.events.EventResolver;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.exceptions.UnknownEventException;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.exceptions.UnknownStateException;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.states.BasicState;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.states.NestedJourneyInvokeState;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.states.State;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.stepresponses.CriStepResponse;
-import uk.gov.di.ipv.core.processjourneyevent.statemachine.stepresponses.JourneyContext;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.stepresponses.PageStepResponse;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.stepresponses.ProcessStepResponse;
 
@@ -31,8 +33,12 @@ public StateMachine(StateMachineInitializer initializer) throws IOException {
     }
 
     public TransitionResult transition(
-            String startState, String event, JourneyContext journeyContext, String currentPage)
-            throws UnknownEventException, UnknownStateException {
+            String startState,
+            String event,
+            String currentPage,
+            EventResolveParameters eventResolveParameters,
+            EventResolver eventResolver)
+            throws UnknownEventException, UnknownStateException, JourneyEngineException {
         var state = states.get(startState.split(JOURNEY_STATE_DELIMITER)[0]);
 
         if (state == null) {
@@ -52,12 +58,15 @@ public TransitionResult transition(
             }
         }
 
-        var result = state.transition(event, startState, journeyContext);
+        var result = state.transition(event, startState, eventResolveParameters, eventResolver);
 
         // Resolve nested journey
         if (result.state() instanceof NestedJourneyInvokeState) {
             var entryEvent = requireNonNullElse(result.targetEntryEvent(), event);
-            var nestedResult = result.state().transition(entryEvent, startState, journeyContext);
+            var nestedResult =
+                    result.state()
+                            .transition(
+                                    entryEvent, startState, eventResolveParameters, eventResolver);
             // Add audit events and context from the outer event
             return new TransitionResult(
                     nestedResult.state(),
diff --git a/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/BasicEvent.java b/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/BasicEvent.java
index 1f2484233f..964f60f74b 100644
--- a/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/BasicEvent.java
+++ b/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/BasicEvent.java
@@ -1,27 +1,17 @@
 package uk.gov.di.ipv.core.processjourneyevent.statemachine.events;
 
 import lombok.Data;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-import software.amazon.awssdk.utils.StringUtils;
 import uk.gov.di.ipv.core.library.auditing.AuditEventTypes;
 import uk.gov.di.ipv.core.library.domain.IpvJourneyTypes;
-import uk.gov.di.ipv.core.processjourneyevent.statemachine.TransitionResult;
-import uk.gov.di.ipv.core.processjourneyevent.statemachine.exceptions.UnknownEventException;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.states.JourneyChangeState;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.states.State;
-import uk.gov.di.ipv.core.processjourneyevent.statemachine.stepresponses.JourneyContext;
 
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Optional;
-
-import static uk.gov.di.ipv.core.library.config.ConfigurationVariable.CREDENTIAL_ISSUER_ENABLED;
 
 @Data
 public class BasicEvent implements Event {
-    private static final Logger LOGGER = LogManager.getLogger();
     private String name;
     private String targetJourney;
     private String targetState;
@@ -32,52 +22,7 @@ public class BasicEvent implements Event {
     private LinkedHashMap<String, Event> checkJourneyContext;
     private List<AuditEventTypes> auditEvents;
     private LinkedHashMap<String, String> auditContext;
-
-    public TransitionResult resolve(JourneyContext journeyContext) throws UnknownEventException {
-        if (checkIfDisabled != null) {
-            Optional<String> firstDisabledCri =
-                    checkIfDisabled.keySet().stream()
-                            .filter(
-                                    id ->
-                                            !journeyContext
-                                                    .configService()
-                                                    .getBooleanParameter(
-                                                            CREDENTIAL_ISSUER_ENABLED, id))
-                            .findFirst();
-            if (firstDisabledCri.isPresent()) {
-                String disabledCriId = firstDisabledCri.get();
-                LOGGER.info("CRI with ID '{}' is disabled. Using alternative event", disabledCriId);
-                return checkIfDisabled.get(disabledCriId).resolve(journeyContext);
-            }
-        }
-        if (checkJourneyContext != null && !StringUtils.isEmpty(journeyContext.name())) {
-            Optional<String> matchingContext =
-                    checkJourneyContext.keySet().stream()
-                            .filter(ctx -> ctx.equals(journeyContext.name()))
-                            .findFirst();
-            if (matchingContext.isPresent()) {
-                String contextValue = matchingContext.get();
-                LOGGER.info("Matching context '{}' is set. Using alternative event", contextValue);
-                return checkJourneyContext.get(contextValue).resolve(journeyContext);
-            }
-        }
-        if (checkFeatureFlag != null) {
-            Optional<String> firstFeatureFlag =
-                    checkFeatureFlag.keySet().stream()
-                            .filter(
-                                    featureFlagValue ->
-                                            journeyContext
-                                                    .configService()
-                                                    .enabled(featureFlagValue))
-                            .findFirst();
-            if (firstFeatureFlag.isPresent()) {
-                String featureFlagValue = firstFeatureFlag.get();
-                LOGGER.info("Feature flag '{}' is set. Using alternative event", featureFlagValue);
-                return checkFeatureFlag.get(featureFlagValue).resolve(journeyContext);
-            }
-        }
-        return new TransitionResult(targetStateObj, auditEvents, auditContext, targetEntryEvent);
-    }
+    private LinkedHashMap<String, Event> checkMitigation;
 
     @Override
     public void initialize(
@@ -104,6 +49,11 @@ public void initialize(
                     (eventName, event) ->
                             initialiseEvent(event, eventName, states, nestedJourneyExitEvents));
         }
+        if (checkMitigation != null) {
+            checkMitigation.forEach(
+                    (eventName, event) ->
+                            initialiseEvent(event, eventName, states, nestedJourneyExitEvents));
+        }
     }
 
     private void initialiseEvent(
diff --git a/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/Event.java b/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/Event.java
index 8fd5d6acfc..438aaf48d1 100644
--- a/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/Event.java
+++ b/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/Event.java
@@ -2,10 +2,7 @@
 
 import com.fasterxml.jackson.annotation.JsonSubTypes;
 import com.fasterxml.jackson.annotation.JsonTypeInfo;
-import uk.gov.di.ipv.core.processjourneyevent.statemachine.TransitionResult;
-import uk.gov.di.ipv.core.processjourneyevent.statemachine.exceptions.UnknownEventException;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.states.State;
-import uk.gov.di.ipv.core.processjourneyevent.statemachine.stepresponses.JourneyContext;
 
 import java.util.Map;
 
@@ -16,7 +13,6 @@
     @JsonSubTypes.Type(value = ExitNestedJourneyEvent.class)
 })
 public interface Event {
-    TransitionResult resolve(JourneyContext journeyContext) throws UnknownEventException;
 
     void initialize(
             String name, Map<String, State> states, Map<String, Event> nestedJourneyExitEvents);
diff --git a/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/EventResolveParameters.java b/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/EventResolveParameters.java
new file mode 100644
index 0000000000..225c535197
--- /dev/null
+++ b/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/EventResolveParameters.java
@@ -0,0 +1,9 @@
+package uk.gov.di.ipv.core.processjourneyevent.statemachine.events;
+
+import uk.gov.di.ipv.core.library.persistence.item.ClientOAuthSessionItem;
+import uk.gov.di.ipv.core.library.persistence.item.IpvSessionItem;
+
+public record EventResolveParameters(
+        String journeyContext,
+        IpvSessionItem ipvSessionItem,
+        ClientOAuthSessionItem clientOAuthSessionItem) {}
diff --git a/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/EventResolver.java b/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/EventResolver.java
new file mode 100644
index 0000000000..0347369bd2
--- /dev/null
+++ b/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/EventResolver.java
@@ -0,0 +1,170 @@
+package uk.gov.di.ipv.core.processjourneyevent.statemachine.events;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import software.amazon.awssdk.utils.StringUtils;
+import uk.gov.di.ipv.core.library.domain.ScopeConstants;
+import uk.gov.di.ipv.core.library.exceptions.CiExtractionException;
+import uk.gov.di.ipv.core.library.exceptions.ConfigException;
+import uk.gov.di.ipv.core.library.exceptions.CredentialParseException;
+import uk.gov.di.ipv.core.library.helpers.LogHelper;
+import uk.gov.di.ipv.core.library.persistence.item.ClientOAuthSessionItem;
+import uk.gov.di.ipv.core.library.service.CimitUtilityService;
+import uk.gov.di.ipv.core.library.service.ConfigService;
+import uk.gov.di.ipv.core.processjourneyevent.exceptions.JourneyEngineException;
+import uk.gov.di.ipv.core.processjourneyevent.statemachine.TransitionResult;
+import uk.gov.di.ipv.core.processjourneyevent.statemachine.exceptions.MissingSecurityCheckCredential;
+import uk.gov.di.ipv.core.processjourneyevent.statemachine.exceptions.UnknownEventException;
+
+import java.text.ParseException;
+import java.util.Optional;
+
+import static uk.gov.di.ipv.core.library.config.ConfigurationVariable.CREDENTIAL_ISSUER_ENABLED;
+import static uk.gov.di.ipv.core.library.domain.ErrorResponse.MISSING_SECURITY_CHECK_CREDENTIAL;
+
+public class EventResolver {
+    private static final Logger LOGGER = LogManager.getLogger();
+    private final CimitUtilityService cimitUtilityService;
+    private final ConfigService configService;
+
+    public EventResolver(CimitUtilityService cimitUtilityService, ConfigService configService) {
+        this.cimitUtilityService = cimitUtilityService;
+        this.configService = configService;
+    }
+
+    public TransitionResult resolve(Event event, EventResolveParameters parameters)
+            throws JourneyEngineException, UnknownEventException {
+        if (event instanceof BasicEvent basicEvent) {
+            return resolveBasicEvent(basicEvent, parameters);
+        } else if (event instanceof ExitNestedJourneyEvent exitNestedJourneyEvent) {
+            return resolveExitNestedJourneyEvent(exitNestedJourneyEvent, parameters);
+        }
+        throw new JourneyEngineException("Unknown event type passed into EventResolver");
+    }
+
+    private TransitionResult resolveExitNestedJourneyEvent(
+            ExitNestedJourneyEvent event, EventResolveParameters parameters)
+            throws JourneyEngineException, UnknownEventException {
+        var exitEventToEmit = event.getExitEventToEmit();
+        Event exitEvent = event.getNestedJourneyExitEvents().get(exitEventToEmit);
+        if (exitEvent == null) {
+            throw new UnknownEventException(
+                    "Event '%s' not found in nested journey's exit events"
+                            .formatted(exitEventToEmit));
+        }
+        return resolve(exitEvent, parameters);
+    }
+
+    private TransitionResult resolveBasicEvent(
+            BasicEvent event, EventResolveParameters resolveParameters)
+            throws UnknownEventException, JourneyEngineException {
+        try {
+            var journeyContext = resolveParameters.journeyContext();
+
+            if (event.getCheckIfDisabled() != null) {
+                var checkIfDisabled = event.getCheckIfDisabled();
+                Optional<String> firstDisabledCri =
+                        checkIfDisabled.keySet().stream()
+                                .filter(
+                                        id ->
+                                                !configService.getBooleanParameter(
+                                                        CREDENTIAL_ISSUER_ENABLED, id))
+                                .findFirst();
+                if (firstDisabledCri.isPresent()) {
+                    String disabledCriId = firstDisabledCri.get();
+                    LOGGER.info(
+                            "CRI with ID '{}' is disabled. Using alternative event", disabledCriId);
+                    return resolve(checkIfDisabled.get(disabledCriId), resolveParameters);
+                }
+            }
+            if (event.getCheckJourneyContext() != null && !StringUtils.isEmpty(journeyContext)) {
+                var checkJourneyContext = event.getCheckJourneyContext();
+                Optional<String> matchingContext =
+                        checkJourneyContext.keySet().stream()
+                                .filter(ctx -> ctx.equals(journeyContext))
+                                .findFirst();
+                if (matchingContext.isPresent()) {
+                    String contextValue = matchingContext.get();
+                    LOGGER.info(
+                            "Matching context '{}' is set. Using alternative event", contextValue);
+                    return resolve(checkJourneyContext.get(contextValue), resolveParameters);
+                }
+            }
+            if (event.getCheckFeatureFlag() != null) {
+                var checkFeatureFlag = event.getCheckFeatureFlag();
+                Optional<String> firstFeatureFlag =
+                        checkFeatureFlag.keySet().stream()
+                                .filter(configService::enabled)
+                                .findFirst();
+                if (firstFeatureFlag.isPresent()) {
+                    String featureFlagValue = firstFeatureFlag.get();
+                    LOGGER.info(
+                            "Feature flag '{}' is set. Using alternative event", featureFlagValue);
+                    return resolve(checkFeatureFlag.get(featureFlagValue), resolveParameters);
+                }
+            }
+
+            if (isCheckMitigationAllowed(event, resolveParameters.clientOAuthSessionItem())) {
+                var checkMitigation = event.getCheckMitigation();
+                var matchedMitigation = getMitigationEvent(event, resolveParameters);
+
+                if (matchedMitigation.isPresent()) {
+                    var mitigationEvent = checkMitigation.get(matchedMitigation.get());
+                    LOGGER.info(
+                            "Mitigation '{}' found. Using alternative event.",
+                            matchedMitigation.get());
+                    return resolve(mitigationEvent, resolveParameters);
+                }
+            }
+
+            return new TransitionResult(
+                    event.getTargetStateObj(),
+                    event.getAuditEvents(),
+                    event.getAuditContext(),
+                    event.getTargetEntryEvent());
+        } catch (MissingSecurityCheckCredential
+                | CiExtractionException
+                | CredentialParseException
+                | ParseException
+                | ConfigException e) {
+            throw new JourneyEngineException("Failed to resolve event", e);
+        }
+    }
+
+    private boolean isCheckMitigationAllowed(
+            BasicEvent event, ClientOAuthSessionItem clientOAuthSessionItem) {
+        var isReverification =
+                !StringUtils.isEmpty(clientOAuthSessionItem.getScope())
+                        && clientOAuthSessionItem
+                                .getScopeClaims()
+                                .contains(ScopeConstants.REVERIFICATION);
+        return event.getCheckMitigation() != null && !isReverification;
+    }
+
+    private Optional<String> getMitigationEvent(
+            BasicEvent event, EventResolveParameters resolveParameters)
+            throws MissingSecurityCheckCredential, CiExtractionException, CredentialParseException,
+                    ParseException, ConfigException {
+        var ipvSessionItem = resolveParameters.ipvSessionItem();
+        var clientOAuthSessionItem = resolveParameters.clientOAuthSessionItem();
+        var securityCheckCredential = ipvSessionItem.getSecurityCheckCredential();
+
+        if (StringUtils.isEmpty(securityCheckCredential)) {
+            LOGGER.error(LogHelper.buildErrorMessage(MISSING_SECURITY_CHECK_CREDENTIAL));
+            throw new MissingSecurityCheckCredential("Missing security check credential");
+        }
+
+        var contraIndicators =
+                cimitUtilityService.getContraIndicatorsFromVc(
+                        securityCheckCredential, clientOAuthSessionItem.getUserId());
+
+        var validMitigation =
+                cimitUtilityService.getMitigationJourneyEvent(
+                        contraIndicators, ipvSessionItem.getTargetVot());
+
+        return (validMitigation.isPresent()
+                        && event.getCheckMitigation().containsKey(validMitigation.get()))
+                ? validMitigation
+                : Optional.empty();
+    }
+}
diff --git a/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/ExitNestedJourneyEvent.java b/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/ExitNestedJourneyEvent.java
index 0206579206..91704dfec2 100644
--- a/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/ExitNestedJourneyEvent.java
+++ b/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/ExitNestedJourneyEvent.java
@@ -1,10 +1,7 @@
 package uk.gov.di.ipv.core.processjourneyevent.statemachine.events;
 
 import lombok.Data;
-import uk.gov.di.ipv.core.processjourneyevent.statemachine.TransitionResult;
-import uk.gov.di.ipv.core.processjourneyevent.statemachine.exceptions.UnknownEventException;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.states.State;
-import uk.gov.di.ipv.core.processjourneyevent.statemachine.stepresponses.JourneyContext;
 
 import java.util.Map;
 
@@ -14,17 +11,6 @@ public class ExitNestedJourneyEvent implements Event {
     private String exitEventToEmit;
     private Map<String, Event> nestedJourneyExitEvents;
 
-    @Override
-    public TransitionResult resolve(JourneyContext journeyContext) throws UnknownEventException {
-        Event event = nestedJourneyExitEvents.get(exitEventToEmit);
-        if (event == null) {
-            throw new UnknownEventException(
-                    "Event '%s' not found in nested journey's exit events"
-                            .formatted(exitEventToEmit));
-        }
-        return event.resolve(journeyContext);
-    }
-
     @Override
     public void initialize(
             String name, Map<String, State> states, Map<String, Event> nestedJourneyExitEvents) {
diff --git a/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/exceptions/MissingSecurityCheckCredential.java b/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/exceptions/MissingSecurityCheckCredential.java
new file mode 100644
index 0000000000..4748c16566
--- /dev/null
+++ b/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/exceptions/MissingSecurityCheckCredential.java
@@ -0,0 +1,11 @@
+package uk.gov.di.ipv.core.processjourneyevent.statemachine.exceptions;
+
+public class MissingSecurityCheckCredential extends Exception {
+    public MissingSecurityCheckCredential() {
+        super();
+    }
+
+    public MissingSecurityCheckCredential(String message) {
+        super(message);
+    }
+}
diff --git a/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/states/BasicState.java b/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/states/BasicState.java
index 77639d473e..3882cac061 100644
--- a/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/states/BasicState.java
+++ b/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/states/BasicState.java
@@ -4,10 +4,13 @@
 import lombok.Data;
 import lombok.NoArgsConstructor;
 import uk.gov.di.ipv.core.library.domain.IpvJourneyTypes;
+import uk.gov.di.ipv.core.processjourneyevent.exceptions.JourneyEngineException;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.TransitionResult;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.events.Event;
+import uk.gov.di.ipv.core.processjourneyevent.statemachine.events.EventResolveParameters;
+import uk.gov.di.ipv.core.processjourneyevent.statemachine.events.EventResolver;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.exceptions.UnknownEventException;
-import uk.gov.di.ipv.core.processjourneyevent.statemachine.stepresponses.JourneyContext;
+import uk.gov.di.ipv.core.processjourneyevent.statemachine.exceptions.UnknownStateException;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.stepresponses.StepResponse;
 
 import java.util.HashMap;
@@ -30,21 +33,25 @@ public class BasicState implements State {
 
     @Override
     public TransitionResult transition(
-            String eventName, String startState, JourneyContext journeyContext)
-            throws UnknownEventException {
+            String eventName,
+            String startState,
+            EventResolveParameters eventResolveParameters,
+            EventResolver eventResolver)
+            throws UnknownEventException, UnknownStateException, JourneyEngineException {
         // Special recovery event
         if (ATTEMPT_RECOVERY_EVENT.equals(eventName)) {
             return new TransitionResult(this);
         }
 
-        return getEvent(eventName)
-                .orElseThrow(
-                        () ->
-                                new UnknownEventException(
-                                        String.format(
-                                                "Unknown event provided to '%s' state: '%s'",
-                                                name, eventName)))
-                .resolve(journeyContext);
+        var event =
+                getEvent(eventName)
+                        .orElseThrow(
+                                () ->
+                                        new UnknownEventException(
+                                                String.format(
+                                                        "Unknown event provided to '%s' state: '%s'",
+                                                        name, eventName)));
+        return eventResolver.resolve(event, eventResolveParameters);
     }
 
     private Optional<Event> getEvent(String eventName) {
diff --git a/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/states/JourneyChangeState.java b/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/states/JourneyChangeState.java
index 7c1bb3e96c..d4649c08ad 100644
--- a/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/states/JourneyChangeState.java
+++ b/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/states/JourneyChangeState.java
@@ -4,9 +4,10 @@
 import lombok.Data;
 import uk.gov.di.ipv.core.library.domain.IpvJourneyTypes;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.TransitionResult;
+import uk.gov.di.ipv.core.processjourneyevent.statemachine.events.EventResolveParameters;
+import uk.gov.di.ipv.core.processjourneyevent.statemachine.events.EventResolver;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.exceptions.UnknownEventException;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.exceptions.UnknownStateException;
-import uk.gov.di.ipv.core.processjourneyevent.statemachine.stepresponses.JourneyContext;
 
 @Data
 @AllArgsConstructor
@@ -16,7 +17,10 @@ public class JourneyChangeState implements State {
 
     @Override
     public TransitionResult transition(
-            String eventName, String startState, JourneyContext journeyContext)
+            String eventName,
+            String startState,
+            EventResolveParameters eventResolveParameters,
+            EventResolver eventResolver)
             throws UnknownEventException, UnknownStateException {
         throw new IllegalStateException("Cannot transition from JourneyChangeState");
     }
diff --git a/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/states/NestedJourneyInvokeState.java b/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/states/NestedJourneyInvokeState.java
index ce9d921221..14c752c1ef 100644
--- a/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/states/NestedJourneyInvokeState.java
+++ b/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/states/NestedJourneyInvokeState.java
@@ -6,11 +6,13 @@
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import uk.gov.di.ipv.core.library.domain.IpvJourneyTypes;
+import uk.gov.di.ipv.core.processjourneyevent.exceptions.JourneyEngineException;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.TransitionResult;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.events.Event;
+import uk.gov.di.ipv.core.processjourneyevent.statemachine.events.EventResolveParameters;
+import uk.gov.di.ipv.core.processjourneyevent.statemachine.events.EventResolver;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.exceptions.UnknownEventException;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.exceptions.UnknownStateException;
-import uk.gov.di.ipv.core.processjourneyevent.statemachine.stepresponses.JourneyContext;
 
 import java.util.Arrays;
 import java.util.LinkedList;
@@ -33,8 +35,11 @@ public class NestedJourneyInvokeState implements State {
 
     @Override
     public TransitionResult transition(
-            String eventName, String startState, JourneyContext journeyContext)
-            throws UnknownEventException, UnknownStateException {
+            String eventName,
+            String startState,
+            EventResolveParameters eventResolveParameters,
+            EventResolver eventResolver)
+            throws UnknownEventException, UnknownStateException, JourneyEngineException {
         Queue<String> stateNameParts = getStateNameParts(startState);
 
         TransitionResult result;
@@ -46,7 +51,7 @@ public TransitionResult transition(
                                 "Unknown entry event '%s' for '%s' state nested journey definition",
                                 eventName, name));
             }
-            result = event.resolve(journeyContext);
+            result = eventResolver.resolve(event, eventResolveParameters);
         } else {
             stateNameParts.remove();
             State currentNestedState =
@@ -61,7 +66,8 @@ public TransitionResult transition(
                     currentNestedState.transition(
                             eventName,
                             String.join(JOURNEY_STATE_DELIMITER, stateNameParts),
-                            journeyContext);
+                            eventResolveParameters,
+                            eventResolver);
         }
 
         if (result.state() instanceof NestedJourneyInvokeState) {
@@ -70,7 +76,8 @@ public TransitionResult transition(
                     .transition(
                             entryEvent,
                             String.join(JOURNEY_STATE_DELIMITER, stateNameParts),
-                            journeyContext);
+                            eventResolveParameters,
+                            eventResolver);
         }
 
         return result;
diff --git a/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/states/State.java b/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/states/State.java
index 3ae23549c8..dd01d65627 100644
--- a/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/states/State.java
+++ b/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/states/State.java
@@ -3,10 +3,12 @@
 import com.fasterxml.jackson.annotation.JsonSubTypes;
 import com.fasterxml.jackson.annotation.JsonTypeInfo;
 import uk.gov.di.ipv.core.library.domain.IpvJourneyTypes;
+import uk.gov.di.ipv.core.processjourneyevent.exceptions.JourneyEngineException;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.TransitionResult;
+import uk.gov.di.ipv.core.processjourneyevent.statemachine.events.EventResolveParameters;
+import uk.gov.di.ipv.core.processjourneyevent.statemachine.events.EventResolver;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.exceptions.UnknownEventException;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.exceptions.UnknownStateException;
-import uk.gov.di.ipv.core.processjourneyevent.statemachine.stepresponses.JourneyContext;
 
 @JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
 @JsonSubTypes({
@@ -14,8 +16,12 @@
     @JsonSubTypes.Type(value = NestedJourneyInvokeState.class),
 })
 public interface State {
-    TransitionResult transition(String eventName, String startState, JourneyContext journeyContext)
-            throws UnknownEventException, UnknownStateException;
+    TransitionResult transition(
+            String eventName,
+            String startState,
+            EventResolveParameters eventResolveParameters,
+            EventResolver eventResolver)
+            throws UnknownEventException, UnknownStateException, JourneyEngineException;
 
     IpvJourneyTypes getJourneyType();
 }
diff --git a/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/stepresponses/JourneyContext.java b/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/stepresponses/JourneyContext.java
deleted file mode 100644
index e620a93bd2..0000000000
--- a/lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/stepresponses/JourneyContext.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package uk.gov.di.ipv.core.processjourneyevent.statemachine.stepresponses;
-
-import uk.gov.di.ipv.core.library.annotations.ExcludeFromGeneratedCoverageReport;
-import uk.gov.di.ipv.core.library.service.ConfigService;
-
-@ExcludeFromGeneratedCoverageReport
-public record JourneyContext(ConfigService configService, String name) {}
diff --git a/lambdas/process-journey-event/src/main/resources/statemachine/test/initial-journey-selection.yaml b/lambdas/process-journey-event/src/main/resources/statemachine/test/initial-journey-selection.yaml
index a20b57c668..0dd848387a 100644
--- a/lambdas/process-journey-event/src/main/resources/statemachine/test/initial-journey-selection.yaml
+++ b/lambdas/process-journey-event/src/main/resources/statemachine/test/initial-journey-selection.yaml
@@ -32,6 +32,11 @@ states:
         targetState: COI_CHECK
       eventEight:
         targetState: STRATEGIC_APP_TRIAGE
+      eventWithMitigation:
+        targetState: ANOTHER_PAGE_STATE
+        checkMitigation:
+          first-mitigation:
+            targetState: CRI_STATE
 
   CRI_STATE:
     response:
@@ -152,6 +157,8 @@ states:
   NESTED_JOURNEY_INVOKE_STATE:
     nestedJourney: NESTED_JOURNEY_DEFINITION
     exitEvents:
+      exitEventFromNestedStateOne:
+        targetState: PAGE_STATE
       exitEventFromNestedStateTwo:
         targetState: ANOTHER_PAGE_STATE
       exitEventFromDoublyNestedInvokeState:
diff --git a/lambdas/process-journey-event/src/main/resources/statemachine/test/nested-journeys/nested-journey-definition.yaml b/lambdas/process-journey-event/src/main/resources/statemachine/test/nested-journeys/nested-journey-definition.yaml
index 7de323708b..4e2a36c223 100644
--- a/lambdas/process-journey-event/src/main/resources/statemachine/test/nested-journeys/nested-journey-definition.yaml
+++ b/lambdas/process-journey-event/src/main/resources/statemachine/test/nested-journeys/nested-journey-definition.yaml
@@ -15,6 +15,9 @@ nestedJourneyStates:
     events:
       eventOne:
         targetState: NESTED_STATE_TWO
+        checkMitigation:
+          first-mitigation:
+            exitEventToEmit: exitEventFromNestedStateOne
   NESTED_STATE_TWO:
     response:
       type: page
diff --git a/lambdas/process-journey-event/src/test/java/uk/gov/di/ipv/core/processjourneyevent/JourneyMapTest.java b/lambdas/process-journey-event/src/test/java/uk/gov/di/ipv/core/processjourneyevent/JourneyMapTest.java
index 261af5e297..0e13dc70c9 100644
--- a/lambdas/process-journey-event/src/test/java/uk/gov/di/ipv/core/processjourneyevent/JourneyMapTest.java
+++ b/lambdas/process-journey-event/src/test/java/uk/gov/di/ipv/core/processjourneyevent/JourneyMapTest.java
@@ -279,6 +279,7 @@ private void checkTargetStatesExist(Map<String, Event> events, Set<String> state
                 checkTargetStatesExist(basicEvent.getCheckIfDisabled(), stateMachineKeys);
                 checkTargetStatesExist(basicEvent.getCheckFeatureFlag(), stateMachineKeys);
                 checkTargetStatesExist(basicEvent.getCheckJourneyContext(), stateMachineKeys);
+                checkTargetStatesExist(basicEvent.getCheckMitigation(), stateMachineKeys);
             }
         }
     }
@@ -297,9 +298,7 @@ private static HashSet<String> getActualExitEvents(NestedJourneyInvokeState stat
                     if (event instanceof BasicEvent basicEvent) {
                         if (basicEvent.getCheckIfDisabled() != null) {
                             basicEvent.getCheckIfDisabled().values().stream()
-                                    .filter(
-                                            disableEvent ->
-                                                    disableEvent instanceof ExitNestedJourneyEvent)
+                                    .filter(ExitNestedJourneyEvent.class::isInstance)
                                     .forEach(
                                             disabledEvent ->
                                                     actualExitEvents.add(
@@ -308,10 +307,7 @@ private static HashSet<String> getActualExitEvents(NestedJourneyInvokeState stat
                         }
                         if (basicEvent.getCheckFeatureFlag() != null) {
                             basicEvent.getCheckFeatureFlag().values().stream()
-                                    .filter(
-                                            checkFlagEvent ->
-                                                    checkFlagEvent
-                                                            instanceof ExitNestedJourneyEvent)
+                                    .filter(ExitNestedJourneyEvent.class::isInstance)
                                     .forEach(
                                             checkFlagEvent ->
                                                     actualExitEvents.add(
@@ -321,10 +317,7 @@ private static HashSet<String> getActualExitEvents(NestedJourneyInvokeState stat
                         }
                         if (basicEvent.getCheckJourneyContext() != null) {
                             basicEvent.getCheckJourneyContext().values().stream()
-                                    .filter(
-                                            checkContextEvent ->
-                                                    checkContextEvent
-                                                            instanceof ExitNestedJourneyEvent)
+                                    .filter(ExitNestedJourneyEvent.class::isInstance)
                                     .forEach(
                                             checkContextEvent ->
                                                     actualExitEvents.add(
@@ -332,6 +325,16 @@ private static HashSet<String> getActualExitEvents(NestedJourneyInvokeState stat
                                                                             checkContextEvent)
                                                                     .getExitEventToEmit()));
                         }
+                        if (basicEvent.getCheckMitigation() != null) {
+                            basicEvent.getCheckMitigation().values().stream()
+                                    .filter(ExitNestedJourneyEvent.class::isInstance)
+                                    .forEach(
+                                            checkMitigationEvent ->
+                                                    actualExitEvents.add(
+                                                            ((ExitNestedJourneyEvent)
+                                                                            checkMitigationEvent)
+                                                                    .getExitEventToEmit()));
+                        }
                     }
                 }
             }
diff --git a/lambdas/process-journey-event/src/test/java/uk/gov/di/ipv/core/processjourneyevent/ProcessJourneyEventHandlerTest.java b/lambdas/process-journey-event/src/test/java/uk/gov/di/ipv/core/processjourneyevent/ProcessJourneyEventHandlerTest.java
index a4a09c26f1..e442c5f579 100644
--- a/lambdas/process-journey-event/src/test/java/uk/gov/di/ipv/core/processjourneyevent/ProcessJourneyEventHandlerTest.java
+++ b/lambdas/process-journey-event/src/test/java/uk/gov/di/ipv/core/processjourneyevent/ProcessJourneyEventHandlerTest.java
@@ -28,23 +28,31 @@
 import uk.gov.di.ipv.core.library.domain.IpvJourneyTypes;
 import uk.gov.di.ipv.core.library.domain.JourneyRequest;
 import uk.gov.di.ipv.core.library.domain.JourneyState;
+import uk.gov.di.ipv.core.library.domain.ScopeConstants;
 import uk.gov.di.ipv.core.library.evcs.service.EvcsService;
+import uk.gov.di.ipv.core.library.exceptions.CiExtractionException;
+import uk.gov.di.ipv.core.library.exceptions.ConfigException;
+import uk.gov.di.ipv.core.library.exceptions.CredentialParseException;
 import uk.gov.di.ipv.core.library.exceptions.IpvSessionNotFoundException;
 import uk.gov.di.ipv.core.library.helpers.SecureTokenHelper;
 import uk.gov.di.ipv.core.library.persistence.item.ClientOAuthSessionItem;
 import uk.gov.di.ipv.core.library.persistence.item.IpvSessionItem;
 import uk.gov.di.ipv.core.library.service.AuditService;
+import uk.gov.di.ipv.core.library.service.CimitUtilityService;
 import uk.gov.di.ipv.core.library.service.ClientOAuthSessionDetailsService;
 import uk.gov.di.ipv.core.library.service.ConfigService;
 import uk.gov.di.ipv.core.library.service.IpvSessionService;
 import uk.gov.di.ipv.core.library.testhelpers.unit.LogCollector;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.NestedJourneyTypes;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.StateMachineInitializerMode;
+import uk.gov.di.model.ContraIndicator;
 
 import java.io.IOException;
+import java.text.ParseException;
 import java.time.Instant;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.stream.Stream;
 
 import static org.hamcrest.MatcherAssert.assertThat;
@@ -68,6 +76,7 @@
 import static uk.gov.di.ipv.core.library.domain.IpvJourneyTypes.SESSION_TIMEOUT;
 import static uk.gov.di.ipv.core.library.domain.IpvJourneyTypes.TECHNICAL_ERROR;
 import static uk.gov.di.ipv.core.library.evcs.enums.EvcsVCState.CURRENT;
+import static uk.gov.di.ipv.core.library.fixtures.TestFixtures.SIGNED_CONTRA_INDICATOR_VC_1;
 import static uk.gov.di.ipv.core.library.fixtures.VcFixtures.M1A_EXPERIAN_FRAUD_VC;
 
 @ExtendWith(MockitoExtension.class)
@@ -108,6 +117,7 @@ class ProcessJourneyEventHandlerTest {
     @Mock private AuditService mockAuditService;
     @Mock private ClientOAuthSessionDetailsService mockClientOAuthSessionService;
     @Mock private EvcsService mockEvcsService;
+    @Mock private CimitUtilityService mockCimitUtilityService;
     @Captor private ArgumentCaptor<AuditEvent> auditEventCaptor;
 
     @AfterEach
@@ -223,7 +233,8 @@ void shouldReturn500IfNoStateMachineMatchingJourneyType() throws Exception {
                         List.of(),
                         StateMachineInitializerMode.STANDARD,
                         REAL_NESTED_JOURNEY_TYPES,
-                        mockEvcsService);
+                        mockEvcsService,
+                        mockCimitUtilityService);
 
         Map<String, Object> output = processJourneyEventHandler.handleRequest(input, mockContext);
 
@@ -267,7 +278,8 @@ void shouldReturnCurrentStateIfPageOutOfSync() throws Exception {
                         List.of(INITIAL_JOURNEY_SELECTION, TECHNICAL_ERROR),
                         StateMachineInitializerMode.TEST,
                         TEST_NESTED_JOURNEY_TYPES,
-                        mockEvcsService);
+                        mockEvcsService,
+                        mockCimitUtilityService);
 
         Map<String, Object> output = processJourneyEventHandler.handleRequest(input, mockContext);
 
@@ -297,7 +309,8 @@ void shouldReturnNextStateIfInSync() throws Exception {
                         List.of(INITIAL_JOURNEY_SELECTION, TECHNICAL_ERROR),
                         StateMachineInitializerMode.TEST,
                         TEST_NESTED_JOURNEY_TYPES,
-                        mockEvcsService);
+                        mockEvcsService,
+                        mockCimitUtilityService);
 
         Map<String, Object> output = processJourneyEventHandler.handleRequest(input, mockContext);
 
@@ -326,7 +339,8 @@ void shouldTransitionCriStateIfCurrentPageMatchesCriId(
                         List.of(INITIAL_JOURNEY_SELECTION, TECHNICAL_ERROR),
                         StateMachineInitializerMode.TEST,
                         TEST_NESTED_JOURNEY_TYPES,
-                        mockEvcsService);
+                        mockEvcsService,
+                        mockCimitUtilityService);
 
         Map<String, Object> output = processJourneyEventHandler.handleRequest(input, mockContext);
 
@@ -369,7 +383,8 @@ void shouldThrowErrorIfJourneyEventDuringProcess() throws Exception {
                         List.of(INITIAL_JOURNEY_SELECTION, TECHNICAL_ERROR),
                         StateMachineInitializerMode.TEST,
                         TEST_NESTED_JOURNEY_TYPES,
-                        mockEvcsService);
+                        mockEvcsService,
+                        mockCimitUtilityService);
 
         Map<String, Object> output = processJourneyEventHandler.handleRequest(input, mockContext);
 
@@ -606,7 +621,8 @@ void shouldUpdateStateStackWithJourneyAndStateWhenStartingNewSubjourney() throws
                         List.of(INITIAL_JOURNEY_SELECTION, TECHNICAL_ERROR),
                         StateMachineInitializerMode.TEST,
                         TEST_NESTED_JOURNEY_TYPES,
-                        mockEvcsService);
+                        mockEvcsService,
+                        mockCimitUtilityService);
 
         processJourneyEventHandler.handleRequest(input, mockContext);
 
@@ -635,7 +651,8 @@ void shouldUpdateStateStackWhenTransitioningWithinSubjourney() throws Exception
                         List.of(INITIAL_JOURNEY_SELECTION),
                         StateMachineInitializerMode.TEST,
                         TEST_NESTED_JOURNEY_TYPES,
-                        mockEvcsService);
+                        mockEvcsService,
+                        mockCimitUtilityService);
 
         processJourneyEventHandler.handleRequest(input, mockContext);
 
@@ -680,7 +697,8 @@ void shouldGoBackIfCurrentAndPreviousStatesArePages() throws Exception {
                         List.of(INITIAL_JOURNEY_SELECTION),
                         StateMachineInitializerMode.TEST,
                         TEST_NESTED_JOURNEY_TYPES,
-                        mockEvcsService);
+                        mockEvcsService,
+                        mockCimitUtilityService);
 
         InOrder inOrder = inOrder(ipvSessionItem, mockIpvSessionService);
 
@@ -746,7 +764,8 @@ void shouldGoBackIfStatesAreInSeparateJourneyMaps() throws Exception {
                         List.of(INITIAL_JOURNEY_SELECTION, TECHNICAL_ERROR),
                         StateMachineInitializerMode.TEST,
                         TEST_NESTED_JOURNEY_TYPES,
-                        mockEvcsService);
+                        mockEvcsService,
+                        mockCimitUtilityService);
 
         var nextResponse =
                 processJourneyEventHandler.handleRequest(inputToNextPageState, mockContext);
@@ -798,7 +817,8 @@ void shouldGoBackIfCurrentPageIsNestedStateAndPreviousIsNot() throws Exception {
                         List.of(INITIAL_JOURNEY_SELECTION, TECHNICAL_ERROR),
                         StateMachineInitializerMode.TEST,
                         TEST_NESTED_JOURNEY_TYPES,
-                        mockEvcsService);
+                        mockEvcsService,
+                        mockCimitUtilityService);
 
         var nextResponse =
                 processJourneyEventHandler.handleRequest(inputToEnterNestedStates, mockContext);
@@ -847,7 +867,8 @@ void shouldRecoredAuditEventWhenEnteringNestedJourney() throws Exception {
                         List.of(INITIAL_JOURNEY_SELECTION, TECHNICAL_ERROR),
                         StateMachineInitializerMode.TEST,
                         TEST_NESTED_JOURNEY_TYPES,
-                        mockEvcsService);
+                        mockEvcsService,
+                        mockCimitUtilityService);
 
         // Act
         var nextResponse =
@@ -898,7 +919,8 @@ void shouldUseBackEventDefinedOnStateIfExists() throws Exception {
                         List.of(INITIAL_JOURNEY_SELECTION, TECHNICAL_ERROR),
                         StateMachineInitializerMode.TEST,
                         TEST_NESTED_JOURNEY_TYPES,
-                        mockEvcsService);
+                        mockEvcsService,
+                        mockCimitUtilityService);
 
         var nextResponse =
                 processJourneyEventHandler.handleRequest(inputToNextPageState, mockContext);
@@ -945,7 +967,8 @@ void shouldReturn500IfPreviousStateNotPageState() throws Exception {
                         List.of(INITIAL_JOURNEY_SELECTION),
                         StateMachineInitializerMode.TEST,
                         TEST_NESTED_JOURNEY_TYPES,
-                        mockEvcsService);
+                        mockEvcsService,
+                        mockCimitUtilityService);
 
         var response = processJourneyEventHandler.handleRequest(input, mockContext);
 
@@ -990,7 +1013,8 @@ void shouldGoBackIfPreviousStateIsIdentifyDeviceState() throws Exception {
                         List.of(INITIAL_JOURNEY_SELECTION),
                         StateMachineInitializerMode.TEST,
                         TEST_NESTED_JOURNEY_TYPES,
-                        mockEvcsService);
+                        mockEvcsService,
+                        mockCimitUtilityService);
 
         processJourneyEventHandler.handleRequest(firstTransitionInput, mockContext);
         assertEquals(
@@ -1032,7 +1056,8 @@ void shouldReturn500IfStateMachineNotFoundWhenCheckingPageState() throws Excepti
                         List.of(INITIAL_JOURNEY_SELECTION),
                         StateMachineInitializerMode.TEST,
                         TEST_NESTED_JOURNEY_TYPES,
-                        mockEvcsService);
+                        mockEvcsService,
+                        mockCimitUtilityService);
 
         var response = processJourneyEventHandler.handleRequest(input, mockContext);
 
@@ -1066,7 +1091,8 @@ void shouldSkipTicfCriOnlyIfDisabled(
                         List.of(INITIAL_JOURNEY_SELECTION, TECHNICAL_ERROR),
                         StateMachineInitializerMode.TEST,
                         TEST_NESTED_JOURNEY_TYPES,
-                        mockEvcsService);
+                        mockEvcsService,
+                        mockCimitUtilityService);
 
         Map<String, Object> output = processJourneyEventHandler.handleRequest(input, mockContext);
 
@@ -1100,7 +1126,8 @@ void shouldSkipCoiCheckOnlyIfNoVcInEvcs(
                         List.of(INITIAL_JOURNEY_SELECTION, TECHNICAL_ERROR),
                         StateMachineInitializerMode.TEST,
                         TEST_NESTED_JOURNEY_TYPES,
-                        mockEvcsService);
+                        mockEvcsService,
+                        mockCimitUtilityService);
 
         var output = processJourneyEventHandler.handleRequest(input, mockContext);
 
@@ -1108,6 +1135,109 @@ void shouldSkipCoiCheckOnlyIfNoVcInEvcs(
         assertEquals(expectedPage, output.get("page"));
     }
 
+    @Test
+    void shouldReturnMitigationStateIfCheckMitigationIsConfigured() throws Exception {
+        var input =
+                JourneyRequest.builder()
+                        .ipAddress(TEST_IP)
+                        .journey("eventWithMitigation")
+                        .ipvSessionId(TEST_SESSION_ID)
+                        .build();
+
+        mockIpvSessionItemAndTimeout("PAGE_STATE");
+        when(mockCimitUtilityService.getContraIndicatorsFromVc(any(), any()))
+                .thenReturn(List.of(new ContraIndicator()));
+        when(mockCimitUtilityService.getMitigationJourneyEvent(any(), any()))
+                .thenReturn(Optional.of("first-mitigation"));
+
+        var processJourneyEventHandler =
+                getProcessJourneyStepHandler(StateMachineInitializerMode.TEST);
+
+        var output = processJourneyEventHandler.handleRequest(input, mockContext);
+
+        assertEquals("/journey/cri/build-oauth-request/aCriId", output.get("journey"));
+    }
+
+    @Test
+    void shouldReturnMitigationStateFromNestedJourneyIfCheckMitigationIsConfigured()
+            throws Exception {
+        var input =
+                JourneyRequest.builder()
+                        .ipAddress(TEST_IP)
+                        .journey("eventOne")
+                        .ipvSessionId(TEST_SESSION_ID)
+                        .build();
+
+        mockIpvSessionItemAndTimeout("NESTED_JOURNEY_INVOKE_STATE/NESTED_STATE_ONE");
+        when(mockCimitUtilityService.getContraIndicatorsFromVc(any(), any()))
+                .thenReturn(List.of(new ContraIndicator()));
+        when(mockCimitUtilityService.getMitigationJourneyEvent(any(), any()))
+                .thenReturn(Optional.of("first-mitigation"));
+
+        var processJourneyEventHandler =
+                getProcessJourneyStepHandler(StateMachineInitializerMode.TEST);
+
+        var output = processJourneyEventHandler.handleRequest(input, mockContext);
+
+        assertEquals("page-id-for-page-state", output.get("page"));
+    }
+
+    private static Stream<Arguments> getContraIndicatorsFromVcErrors() {
+        return Stream.of(
+                Arguments.of(new ParseException("Unable to parse vc string", 0)),
+                Arguments.of(new CredentialParseException("Unable to parse credentials")),
+                Arguments.of(new CiExtractionException("Unable to extract CIs from VC")));
+    }
+
+    @ParameterizedTest
+    @MethodSource("getContraIndicatorsFromVcErrors")
+    void shouldReturn500IfCimitUtilityServiceThrowsExceptionWhenGettingContraIndicators(
+            Exception exception) throws Exception {
+        var input =
+                JourneyRequest.builder()
+                        .ipAddress(TEST_IP)
+                        .journey("eventWithMitigation")
+                        .ipvSessionId(TEST_SESSION_ID)
+                        .build();
+
+        mockIpvSessionItemAndTimeout("PAGE_STATE");
+        when(mockCimitUtilityService.getContraIndicatorsFromVc(any(), any())).thenThrow(exception);
+
+        var processJourneyEventHandler =
+                getProcessJourneyStepHandler(StateMachineInitializerMode.TEST);
+
+        var response = processJourneyEventHandler.handleRequest(input, mockContext);
+
+        assertEquals(HttpStatusCode.INTERNAL_SERVER_ERROR, response.get(STATUS_CODE));
+        assertEquals(ErrorResponse.FAILED_JOURNEY_ENGINE_STEP.getCode(), response.get(CODE));
+        assertEquals(ErrorResponse.FAILED_JOURNEY_ENGINE_STEP.getMessage(), response.get(MESSAGE));
+    }
+
+    @Test
+    void shouldReturn500IfCimitUtilityServiceThrowsConfigException() throws Exception {
+        var input =
+                JourneyRequest.builder()
+                        .ipAddress(TEST_IP)
+                        .journey("eventWithMitigation")
+                        .ipvSessionId(TEST_SESSION_ID)
+                        .build();
+
+        mockIpvSessionItemAndTimeout("PAGE_STATE");
+        when(mockCimitUtilityService.getContraIndicatorsFromVc(any(), any()))
+                .thenReturn(List.of(new ContraIndicator()));
+        when(mockCimitUtilityService.getMitigationJourneyEvent(any(), any()))
+                .thenThrow(new ConfigException("Unable to get CIMIT config."));
+
+        var processJourneyEventHandler =
+                getProcessJourneyStepHandler(StateMachineInitializerMode.TEST);
+
+        var response = processJourneyEventHandler.handleRequest(input, mockContext);
+
+        assertEquals(HttpStatusCode.INTERNAL_SERVER_ERROR, response.get(STATUS_CODE));
+        assertEquals(ErrorResponse.FAILED_JOURNEY_ENGINE_STEP.getCode(), response.get(CODE));
+        assertEquals(ErrorResponse.FAILED_JOURNEY_ENGINE_STEP.getMessage(), response.get(MESSAGE));
+    }
+
     @Test
     void shouldLogRuntimeExceptionsAndRethrow() throws Exception {
         // Arrange
@@ -1129,7 +1259,8 @@ void shouldLogRuntimeExceptionsAndRethrow() throws Exception {
                         List.of(INITIAL_JOURNEY_SELECTION, TECHNICAL_ERROR),
                         StateMachineInitializerMode.TEST,
                         TEST_NESTED_JOURNEY_TYPES,
-                        mockEvcsService);
+                        mockEvcsService,
+                        mockCimitUtilityService);
 
         var logCollector = LogCollector.getLogCollectorFor(ProcessJourneyEventHandler.class);
 
@@ -1153,6 +1284,7 @@ private void mockIpvSessionItemAndTimeout(String userState) throws Exception {
         ipvSessionItem.setCreationDateTime(Instant.now().toString());
         ipvSessionItem.pushState(new JourneyState(INITIAL_JOURNEY_SELECTION, userState));
         ipvSessionItem.setClientOAuthSessionId(SecureTokenHelper.getInstance().generate());
+        ipvSessionItem.setSecurityCheckCredential(SIGNED_CONTRA_INDICATOR_VC_1);
 
         when(mockConfigService.getParameter(COMPONENT_ID)).thenReturn("core");
         when(mockConfigService.getLongParameter(BACKEND_SESSION_TIMEOUT)).thenReturn(7200L);
@@ -1170,6 +1302,8 @@ private ClientOAuthSessionItem getClientOAuthSessionItem() {
                 .govukSigninJourneyId("testjourneyid")
                 .userId(TEST_USER_ID)
                 .evcsAccessToken(TEST_EVCS_ACCESS_TOKEN)
+                .vtr(List.of("P2"))
+                .scope(ScopeConstants.OPENID)
                 .build();
     }
 
@@ -1194,7 +1328,8 @@ private ProcessJourneyEventHandler getProcessJourneyStepHandler(
                 journeyTypes,
                 stateMachineInitializerMode,
                 nestedJourneyTypes,
-                mockEvcsService);
+                mockEvcsService,
+                mockCimitUtilityService);
     }
 
     private ProcessJourneyEventHandler getProcessJourneyStepHandler() throws IOException {
diff --git a/lambdas/process-journey-event/src/test/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/StateMachineTest.java b/lambdas/process-journey-event/src/test/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/StateMachineTest.java
index 1a4fa2f3bd..d59abf01bc 100644
--- a/lambdas/process-journey-event/src/test/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/StateMachineTest.java
+++ b/lambdas/process-journey-event/src/test/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/StateMachineTest.java
@@ -1,15 +1,18 @@
 package uk.gov.di.ipv.core.processjourneyevent.statemachine;
 
 import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
 import uk.gov.di.ipv.core.library.auditing.AuditEventTypes;
 import uk.gov.di.ipv.core.library.domain.JourneyState;
-import uk.gov.di.ipv.core.library.service.ConfigService;
+import uk.gov.di.ipv.core.library.persistence.item.ClientOAuthSessionItem;
+import uk.gov.di.ipv.core.library.persistence.item.IpvSessionItem;
+import uk.gov.di.ipv.core.processjourneyevent.statemachine.events.EventResolveParameters;
+import uk.gov.di.ipv.core.processjourneyevent.statemachine.events.EventResolver;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.exceptions.UnknownStateException;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.states.BasicState;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.states.NestedJourneyDefinition;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.states.NestedJourneyInvokeState;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.states.State;
-import uk.gov.di.ipv.core.processjourneyevent.statemachine.stepresponses.JourneyContext;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.stepresponses.PageStepResponse;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.stepresponses.ProcessStepResponse;
 
@@ -25,15 +28,18 @@
 import static uk.gov.di.ipv.core.library.domain.IpvJourneyTypes.INITIAL_JOURNEY_SELECTION;
 
 class StateMachineTest {
-    private static final JourneyContext JOURNEY_CONTEXT =
-            new JourneyContext(mock(ConfigService.class), "");
+    private static final EventResolveParameters EVENT_RESOLVE_PARAMETERS =
+            new EventResolveParameters(
+                    "journeyContext", new IpvSessionItem(), new ClientOAuthSessionItem());
+    @Mock private EventResolver eventResolver;
 
     @Test
     void transitionShouldReturnAppropriateState() throws Exception {
         var expectedResult = new TransitionResult(new BasicState());
 
         State startingState = mock(BasicState.class);
-        when(startingState.transition("event", "START_STATE", JOURNEY_CONTEXT))
+        when(startingState.transition(
+                        "event", "START_STATE", EVENT_RESOLVE_PARAMETERS, eventResolver))
                 .thenReturn(expectedResult);
 
         StateMachineInitializer mockStateMachineInitializer = mock(StateMachineInitializer.class);
@@ -42,7 +48,9 @@ void transitionShouldReturnAppropriateState() throws Exception {
 
         StateMachine stateMachine = new StateMachine(mockStateMachineInitializer);
 
-        var actualResult = stateMachine.transition("START_STATE", "event", JOURNEY_CONTEXT, null);
+        var actualResult =
+                stateMachine.transition(
+                        "START_STATE", "event", null, EVENT_RESOLVE_PARAMETERS, eventResolver);
 
         assertEquals(expectedResult, actualResult);
     }
@@ -57,18 +65,26 @@ void transitionShouldThrowIfGivenAnUnknownState() throws Exception {
 
         assertThrows(
                 UnknownStateException.class,
-                () -> stateMachine.transition("UNKNOWN_STATE", "event", JOURNEY_CONTEXT, null));
+                () ->
+                        stateMachine.transition(
+                                "UNKNOWN_STATE",
+                                "event",
+                                null,
+                                EVENT_RESOLVE_PARAMETERS,
+                                eventResolver));
     }
 
     @Test
     void transitionShouldTransitionIntoNestedJourneyInvokeState() throws Exception {
         var expectedResult = new TransitionResult(new BasicState());
         State nestedJourneyInvokeState = mock(NestedJourneyInvokeState.class);
-        when(nestedJourneyInvokeState.transition("event", "START_STATE", JOURNEY_CONTEXT))
+        when(nestedJourneyInvokeState.transition(
+                        "event", "START_STATE", EVENT_RESOLVE_PARAMETERS, eventResolver))
                 .thenReturn(expectedResult);
 
         State startingState = mock(BasicState.class);
-        when(startingState.transition("event", "START_STATE", JOURNEY_CONTEXT))
+        when(startingState.transition(
+                        "event", "START_STATE", EVENT_RESOLVE_PARAMETERS, eventResolver))
                 .thenReturn(new TransitionResult(nestedJourneyInvokeState));
 
         StateMachineInitializer mockStateMachineInitializer = mock(StateMachineInitializer.class);
@@ -77,7 +93,9 @@ void transitionShouldTransitionIntoNestedJourneyInvokeState() throws Exception {
 
         StateMachine stateMachine = new StateMachine(mockStateMachineInitializer);
 
-        var actualResult = stateMachine.transition("START_STATE", "event", JOURNEY_CONTEXT, null);
+        var actualResult =
+                stateMachine.transition(
+                        "START_STATE", "event", null, EVENT_RESOLVE_PARAMETERS, eventResolver);
 
         assertEquals(expectedResult, actualResult);
     }
@@ -88,11 +106,13 @@ void transitionShouldPreserveAuditEventsAndContextFromFirstTransitionIntoNestedJ
         // Arrange
         var nestedResult = new TransitionResult(new BasicState());
         State nestedJourneyInvokeState = mock(NestedJourneyInvokeState.class);
-        when(nestedJourneyInvokeState.transition("event", "START_STATE", JOURNEY_CONTEXT))
+        when(nestedJourneyInvokeState.transition(
+                        "event", "START_STATE", EVENT_RESOLVE_PARAMETERS, eventResolver))
                 .thenReturn(nestedResult);
 
         State startingState = mock(BasicState.class);
-        when(startingState.transition("event", "START_STATE", JOURNEY_CONTEXT))
+        when(startingState.transition(
+                        "event", "START_STATE", EVENT_RESOLVE_PARAMETERS, eventResolver))
                 .thenReturn(
                         new TransitionResult(
                                 nestedJourneyInvokeState,
@@ -107,7 +127,9 @@ void transitionShouldPreserveAuditEventsAndContextFromFirstTransitionIntoNestedJ
         StateMachine stateMachine = new StateMachine(mockStateMachineInitializer);
 
         // Act
-        var actualResult = stateMachine.transition("START_STATE", "event", JOURNEY_CONTEXT, null);
+        var actualResult =
+                stateMachine.transition(
+                        "START_STATE", "event", null, EVENT_RESOLVE_PARAMETERS, eventResolver);
 
         // Assert
         var expectedResult =
@@ -130,11 +152,13 @@ void transitionShouldPreserveAuditEventsAndContextFromSecondTransitionIntoNested
                         Map.of("testKey", "testValue"),
                         null);
         State nestedJourneyInvokeState = mock(NestedJourneyInvokeState.class);
-        when(nestedJourneyInvokeState.transition("event", "START_STATE", JOURNEY_CONTEXT))
+        when(nestedJourneyInvokeState.transition(
+                        "event", "START_STATE", EVENT_RESOLVE_PARAMETERS, eventResolver))
                 .thenReturn(nestedResult);
 
         State startingState = mock(BasicState.class);
-        when(startingState.transition("event", "START_STATE", JOURNEY_CONTEXT))
+        when(startingState.transition(
+                        "event", "START_STATE", EVENT_RESOLVE_PARAMETERS, eventResolver))
                 .thenReturn(new TransitionResult(nestedJourneyInvokeState));
 
         StateMachineInitializer mockStateMachineInitializer = mock(StateMachineInitializer.class);
@@ -144,7 +168,9 @@ void transitionShouldPreserveAuditEventsAndContextFromSecondTransitionIntoNested
         StateMachine stateMachine = new StateMachine(mockStateMachineInitializer);
 
         // Act
-        var actualResult = stateMachine.transition("START_STATE", "event", JOURNEY_CONTEXT, null);
+        var actualResult =
+                stateMachine.transition(
+                        "START_STATE", "event", null, EVENT_RESOLVE_PARAMETERS, eventResolver);
 
         // Assert
         var expectedResult =
@@ -167,11 +193,13 @@ void transitionShouldCombineAuditEventsAndContextFromFirstAndSecondTransitionInt
                         Map.of("testKey1", "testValue1"),
                         null);
         State nestedJourneyInvokeState = mock(NestedJourneyInvokeState.class);
-        when(nestedJourneyInvokeState.transition("event", "START_STATE", JOURNEY_CONTEXT))
+        when(nestedJourneyInvokeState.transition(
+                        "event", "START_STATE", EVENT_RESOLVE_PARAMETERS, eventResolver))
                 .thenReturn(nestedResult);
 
         State startingState = mock(BasicState.class);
-        when(startingState.transition("event", "START_STATE", JOURNEY_CONTEXT))
+        when(startingState.transition(
+                        "event", "START_STATE", EVENT_RESOLVE_PARAMETERS, eventResolver))
                 .thenReturn(
                         new TransitionResult(
                                 nestedJourneyInvokeState,
@@ -186,7 +214,9 @@ void transitionShouldCombineAuditEventsAndContextFromFirstAndSecondTransitionInt
         StateMachine stateMachine = new StateMachine(mockStateMachineInitializer);
 
         // Act
-        var actualResult = stateMachine.transition("START_STATE", "event", JOURNEY_CONTEXT, null);
+        var actualResult =
+                stateMachine.transition(
+                        "START_STATE", "event", null, EVENT_RESOLVE_PARAMETERS, eventResolver);
 
         // Assert
         var expectedResult =
@@ -209,12 +239,14 @@ void transitionShouldTransitionIntoNestedJourneyInvokeStateWithEntryEventOverrid
         // Nested event transitions using the override
         var expectedResult = new TransitionResult(new BasicState());
         State nestedJourneyInvokeState = mock(NestedJourneyInvokeState.class);
-        when(nestedJourneyInvokeState.transition(eventOverride, "START_STATE", JOURNEY_CONTEXT))
+        when(nestedJourneyInvokeState.transition(
+                        eventOverride, "START_STATE", EVENT_RESOLVE_PARAMETERS, eventResolver))
                 .thenReturn(expectedResult);
 
         // Outer event transitions using the overall event
         State startingState = mock(BasicState.class);
-        when(startingState.transition(event, "START_STATE", JOURNEY_CONTEXT))
+        when(startingState.transition(
+                        event, "START_STATE", EVENT_RESOLVE_PARAMETERS, eventResolver))
                 .thenReturn(
                         new TransitionResult(nestedJourneyInvokeState, null, null, eventOverride));
 
@@ -224,7 +256,9 @@ void transitionShouldTransitionIntoNestedJourneyInvokeStateWithEntryEventOverrid
 
         StateMachine stateMachine = new StateMachine(mockStateMachineInitializer);
 
-        var actualResult = stateMachine.transition("START_STATE", event, JOURNEY_CONTEXT, null);
+        var actualResult =
+                stateMachine.transition(
+                        "START_STATE", event, null, EVENT_RESOLVE_PARAMETERS, eventResolver);
 
         assertEquals(expectedResult, actualResult);
     }
@@ -234,7 +268,11 @@ void transitionShouldHandleNestedStateName() throws Exception {
         var expectedResult = new TransitionResult(new BasicState());
 
         State startingState = mock(NestedJourneyInvokeState.class);
-        when(startingState.transition("event", "START_STATE/NESTED_JOURNEY", JOURNEY_CONTEXT))
+        when(startingState.transition(
+                        "event",
+                        "START_STATE/NESTED_JOURNEY",
+                        EVENT_RESOLVE_PARAMETERS,
+                        eventResolver))
                 .thenReturn(expectedResult);
 
         StateMachineInitializer mockStateMachineInitializer = mock(StateMachineInitializer.class);
@@ -245,7 +283,11 @@ void transitionShouldHandleNestedStateName() throws Exception {
 
         var actualResult =
                 stateMachine.transition(
-                        "START_STATE/NESTED_JOURNEY", "event", JOURNEY_CONTEXT, null);
+                        "START_STATE/NESTED_JOURNEY",
+                        "event",
+                        null,
+                        EVENT_RESOLVE_PARAMETERS,
+                        eventResolver);
 
         assertEquals(expectedResult, actualResult);
     }
diff --git a/lambdas/process-journey-event/src/test/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/BasicEventTest.java b/lambdas/process-journey-event/src/test/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/BasicEventTest.java
index 42b77ba134..f327e169de 100644
--- a/lambdas/process-journey-event/src/test/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/BasicEventTest.java
+++ b/lambdas/process-journey-event/src/test/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/BasicEventTest.java
@@ -2,110 +2,16 @@
 
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
-import uk.gov.di.ipv.core.library.config.CoreFeatureFlag;
-import uk.gov.di.ipv.core.library.service.ConfigService;
-import uk.gov.di.ipv.core.processjourneyevent.statemachine.TransitionResult;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.states.BasicState;
-import uk.gov.di.ipv.core.processjourneyevent.statemachine.stepresponses.JourneyContext;
 
 import java.util.LinkedHashMap;
 import java.util.Map;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.mockito.Mockito.when;
-import static uk.gov.di.ipv.core.library.config.ConfigurationVariable.CREDENTIAL_ISSUER_ENABLED;
 
 @ExtendWith(MockitoExtension.class)
 class BasicEventTest {
-    @Mock private ConfigService mockConfigService;
-    @InjectMocks private JourneyContext journeyContext;
-
-    @Test
-    void resolveShouldReturnAState() throws Exception {
-        var expectedResult = new TransitionResult(new BasicState(), null, null, null);
-        BasicEvent basicEvent = new BasicEvent();
-        basicEvent.setTargetStateObj(expectedResult.state());
-
-        assertEquals(expectedResult, basicEvent.resolve(journeyContext));
-    }
-
-    @Test
-    void resolveShouldReturnAlternativeStateIfACheckedCriIsDisabled() throws Exception {
-        BasicEvent basicEventWithCheckIfDisabledConfigured = new BasicEvent();
-        basicEventWithCheckIfDisabledConfigured.setTargetStateObj(new BasicState());
-
-        BasicEvent alternativeEvent = new BasicEvent();
-        BasicState alternativeTargetState = new BasicState();
-        alternativeEvent.setTargetStateObj(alternativeTargetState);
-
-        when(mockConfigService.getBooleanParameter(CREDENTIAL_ISSUER_ENABLED, "anEnabledCri"))
-                .thenReturn(true);
-        when(mockConfigService.getBooleanParameter(CREDENTIAL_ISSUER_ENABLED, "aDisabledCri"))
-                .thenReturn(false);
-        LinkedHashMap<String, Event> checkIfDisabled = new LinkedHashMap<>();
-        checkIfDisabled.put("anEnabledCri", new BasicEvent());
-        checkIfDisabled.put("aDisabledCri", alternativeEvent);
-        basicEventWithCheckIfDisabledConfigured.setCheckIfDisabled(checkIfDisabled);
-
-        var result = basicEventWithCheckIfDisabledConfigured.resolve(journeyContext);
-
-        assertEquals(alternativeTargetState, result.state());
-    }
-
-    @Test
-    void resolveShouldReturnAlternativeStateIfACheckedFeatureFlagIsSet() throws Exception {
-        BasicEvent eventWithCheckFeatureFlagConfigured = new BasicEvent();
-        BasicState featureFlagTargetState = new BasicState();
-        eventWithCheckFeatureFlagConfigured.setTargetStateObj(featureFlagTargetState);
-
-        BasicEvent defaultEvent = new BasicEvent();
-        defaultEvent.setTargetStateObj(new BasicState());
-
-        LinkedHashMap<String, Event> checkFeatureFlag = new LinkedHashMap<>();
-        checkFeatureFlag.put(
-                CoreFeatureFlag.UNUSED_PLACEHOLDER.getName(), eventWithCheckFeatureFlagConfigured);
-        defaultEvent.setCheckFeatureFlag(checkFeatureFlag);
-
-        var result = defaultEvent.resolve(journeyContext);
-
-        assertEquals(featureFlagTargetState, result.state());
-    }
-
-    @Test
-    void resolveShouldReturnAlternativeStateIfJourneyContextIsSetAndIgnoreFeatureFlag()
-            throws Exception {
-        BasicEvent eventWithCheckFeatureFlagConfigured = new BasicEvent();
-        BasicState featureFlagTargetState = new BasicState();
-        eventWithCheckFeatureFlagConfigured.setTargetStateObj(featureFlagTargetState);
-
-        BasicEvent eventWithContextConfigured = new BasicEvent();
-        BasicState contextTargetState =
-                new BasicState("evtWithContext", "", "test-context", null, null, null, null);
-        eventWithContextConfigured.setTargetStateObj(contextTargetState);
-
-        BasicEvent defaultEvent = new BasicEvent();
-        defaultEvent.setTargetStateObj(
-                new BasicState("defaultEvent", "", "", null, null, null, null));
-
-        LinkedHashMap<String, Event> checkFeatureFlag = new LinkedHashMap<>();
-        checkFeatureFlag.put(
-                CoreFeatureFlag.UNUSED_PLACEHOLDER.getName(), eventWithCheckFeatureFlagConfigured);
-        defaultEvent.setCheckFeatureFlag(checkFeatureFlag);
-
-        LinkedHashMap<String, Event> checkContext = new LinkedHashMap<>();
-        checkContext.put("test-context", eventWithContextConfigured);
-
-        defaultEvent.setCheckJourneyContext(checkContext);
-
-        var journeyContextWithName = new JourneyContext(mockConfigService, "test-context");
-        var result = defaultEvent.resolve(journeyContextWithName);
-
-        assertEquals(contextTargetState, result.state());
-    }
-
     @Test
     void initializeShouldSetAttributes() {
         var basicEvent = new BasicEvent();
diff --git a/lambdas/process-journey-event/src/test/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/EventResolverTest.java b/lambdas/process-journey-event/src/test/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/EventResolverTest.java
new file mode 100644
index 0000000000..c80534e67e
--- /dev/null
+++ b/lambdas/process-journey-event/src/test/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/EventResolverTest.java
@@ -0,0 +1,357 @@
+package uk.gov.di.ipv.core.processjourneyevent.statemachine.events;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import uk.gov.di.ipv.core.library.config.CoreFeatureFlag;
+import uk.gov.di.ipv.core.library.domain.ScopeConstants;
+import uk.gov.di.ipv.core.library.persistence.item.ClientOAuthSessionItem;
+import uk.gov.di.ipv.core.library.persistence.item.IpvSessionItem;
+import uk.gov.di.ipv.core.library.service.CimitUtilityService;
+import uk.gov.di.ipv.core.library.service.ConfigService;
+import uk.gov.di.ipv.core.processjourneyevent.exceptions.JourneyEngineException;
+import uk.gov.di.ipv.core.processjourneyevent.statemachine.TransitionResult;
+import uk.gov.di.ipv.core.processjourneyevent.statemachine.exceptions.UnknownEventException;
+import uk.gov.di.ipv.core.processjourneyevent.statemachine.states.BasicState;
+import uk.gov.di.model.ContraIndicator;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static uk.gov.di.ipv.core.library.config.ConfigurationVariable.CREDENTIAL_ISSUER_ENABLED;
+import static uk.gov.di.ipv.core.library.enums.Vot.P2;
+import static uk.gov.di.ipv.core.library.fixtures.TestFixtures.SIGNED_CONTRA_INDICATOR_VC_1;
+
+@ExtendWith(MockitoExtension.class)
+public class EventResolverTest {
+    @Mock private ConfigService mockConfigService;
+    @Mock private CimitUtilityService mockCimitUtilityService;
+    @InjectMocks private EventResolver eventResolver;
+
+    private ClientOAuthSessionItem clientOAuthSessionItem;
+    private EventResolveParameters eventResolveParameters;
+
+    @Nested
+    class BasicEventTests {
+        @BeforeEach
+        void setUp() {
+            clientOAuthSessionItem =
+                    ClientOAuthSessionItem.builder()
+                            .userId("user-id")
+                            .vtr(List.of(P2.name()))
+                            .scope(ScopeConstants.OPENID)
+                            .build();
+
+            eventResolveParameters =
+                    new EventResolveParameters("", new IpvSessionItem(), clientOAuthSessionItem);
+        }
+
+        @Test
+        void resolveShouldReturnAState() throws Exception {
+            var expectedResult = new TransitionResult(new BasicState(), null, null, null);
+            BasicEvent basicEvent = new BasicEvent();
+            basicEvent.setTargetStateObj(expectedResult.state());
+
+            assertEquals(expectedResult, eventResolver.resolve(basicEvent, eventResolveParameters));
+        }
+
+        @Test
+        void resolveShouldReturnAlternativeStateIfACheckedCriIsDisabled() throws Exception {
+            BasicEvent basicEventWithCheckIfDisabledConfigured = new BasicEvent();
+            basicEventWithCheckIfDisabledConfigured.setTargetStateObj(new BasicState());
+
+            BasicEvent alternativeEvent = new BasicEvent();
+            BasicState alternativeTargetState = new BasicState();
+            alternativeEvent.setTargetStateObj(alternativeTargetState);
+
+            when(mockConfigService.getBooleanParameter(CREDENTIAL_ISSUER_ENABLED, "anEnabledCri"))
+                    .thenReturn(true);
+            when(mockConfigService.getBooleanParameter(CREDENTIAL_ISSUER_ENABLED, "aDisabledCri"))
+                    .thenReturn(false);
+            LinkedHashMap<String, Event> checkIfDisabled = new LinkedHashMap<>();
+            checkIfDisabled.put("anEnabledCri", new BasicEvent());
+            checkIfDisabled.put("aDisabledCri", alternativeEvent);
+            basicEventWithCheckIfDisabledConfigured.setCheckIfDisabled(checkIfDisabled);
+
+            var result =
+                    eventResolver.resolve(
+                            basicEventWithCheckIfDisabledConfigured, eventResolveParameters);
+
+            assertEquals(alternativeTargetState, result.state());
+        }
+
+        @Test
+        void resolveShouldReturnAlternativeStateIfACheckedFeatureFlagIsSet() throws Exception {
+            BasicEvent eventWithCheckFeatureFlagConfigured = new BasicEvent();
+            BasicState featureFlagTargetState = new BasicState();
+            eventWithCheckFeatureFlagConfigured.setTargetStateObj(featureFlagTargetState);
+
+            BasicEvent defaultEvent = new BasicEvent();
+            defaultEvent.setTargetStateObj(new BasicState());
+
+            LinkedHashMap<String, Event> checkFeatureFlag = new LinkedHashMap<>();
+            checkFeatureFlag.put(
+                    CoreFeatureFlag.UNUSED_PLACEHOLDER.getName(),
+                    eventWithCheckFeatureFlagConfigured);
+            defaultEvent.setCheckFeatureFlag(checkFeatureFlag);
+
+            var result = eventResolver.resolve(defaultEvent, eventResolveParameters);
+
+            assertEquals(featureFlagTargetState, result.state());
+        }
+
+        @Test
+        void resolveShouldReturnAlternativeStateIfJourneyContextIsSetAndIgnoreFeatureFlag()
+                throws Exception {
+            BasicEvent eventWithCheckFeatureFlagConfigured = new BasicEvent();
+            BasicState featureFlagTargetState = new BasicState();
+            eventWithCheckFeatureFlagConfigured.setTargetStateObj(featureFlagTargetState);
+
+            BasicEvent eventWithContextConfigured = new BasicEvent();
+            BasicState contextTargetState =
+                    new BasicState("evtWithContext", "", "test-context", null, null, null, null);
+            eventWithContextConfigured.setTargetStateObj(contextTargetState);
+
+            BasicEvent defaultEvent = new BasicEvent();
+            defaultEvent.setTargetStateObj(
+                    new BasicState("defaultEvent", "", "", null, null, null, null));
+
+            LinkedHashMap<String, Event> checkFeatureFlag = new LinkedHashMap<>();
+            checkFeatureFlag.put(
+                    CoreFeatureFlag.UNUSED_PLACEHOLDER.getName(),
+                    eventWithCheckFeatureFlagConfigured);
+            defaultEvent.setCheckFeatureFlag(checkFeatureFlag);
+
+            LinkedHashMap<String, Event> checkContext = new LinkedHashMap<>();
+            checkContext.put("test-context", eventWithContextConfigured);
+
+            defaultEvent.setCheckJourneyContext(checkContext);
+
+            var testParams =
+                    new EventResolveParameters(
+                            "test-context",
+                            new IpvSessionItem(),
+                            ClientOAuthSessionItem.builder().scope(ScopeConstants.OPENID).build());
+            var result = eventResolver.resolve(defaultEvent, testParams);
+
+            assertEquals(contextTargetState, result.state());
+        }
+
+        @Nested
+        class CheckMitigationConfigured {
+            private List<ContraIndicator> testCis;
+            private IpvSessionItem ipvSessionItem;
+
+            @BeforeEach
+            void setUp() {
+                testCis = List.of(new ContraIndicator());
+
+                ipvSessionItem = new IpvSessionItem();
+                ipvSessionItem.setSecurityCheckCredential(SIGNED_CONTRA_INDICATOR_VC_1);
+            }
+
+            @Test
+            void
+                    resolveShouldReturnAlternativeStateIfMitigationJourneyIsFoundAndExistsInCheckMitigation()
+                            throws Exception {
+                // Arrange
+                var basicEventWithCheckMitigationConfigured = new BasicEvent();
+
+                var alternativeEvent = new BasicEvent();
+                var alternativeTargetState = new BasicState();
+                alternativeEvent.setTargetStateObj(alternativeTargetState);
+
+                LinkedHashMap<String, Event> checkMitigation = new LinkedHashMap<>();
+                checkMitigation.put("first-mitigation", alternativeEvent);
+
+                basicEventWithCheckMitigationConfigured.setCheckMitigation(checkMitigation);
+
+                when(mockCimitUtilityService.getContraIndicatorsFromVc(
+                                SIGNED_CONTRA_INDICATOR_VC_1, clientOAuthSessionItem.getUserId()))
+                        .thenReturn(testCis);
+                when(mockCimitUtilityService.getMitigationJourneyEvent(eq(testCis), any()))
+                        .thenReturn(Optional.of("first-mitigation"));
+
+                // Act
+                var result =
+                        eventResolver.resolve(
+                                basicEventWithCheckMitigationConfigured,
+                                new EventResolveParameters(
+                                        "journeyContext", ipvSessionItem, clientOAuthSessionItem));
+
+                // Assert
+                assertEquals(alternativeTargetState, result.state());
+            }
+
+            @Test
+            void
+                    resolveShouldReturnTargetStateIfMitigationJourneyIsFoundButDoesNotExistInCheckMitigation()
+                            throws Exception {
+                // Arrange
+                var basicEventWithCheckMitigationConfigured = new BasicEvent();
+                var originalTargetStateObj = new BasicState();
+                basicEventWithCheckMitigationConfigured.setTargetStateObj(originalTargetStateObj);
+
+                LinkedHashMap<String, Event> checkMitigation = new LinkedHashMap<>();
+                checkMitigation.put("first-mitigation", new BasicEvent());
+                basicEventWithCheckMitigationConfigured.setCheckMitigation(checkMitigation);
+
+                when(mockCimitUtilityService.getContraIndicatorsFromVc(
+                                SIGNED_CONTRA_INDICATOR_VC_1, clientOAuthSessionItem.getUserId()))
+                        .thenReturn(testCis);
+                when(mockCimitUtilityService.getMitigationJourneyEvent(eq(testCis), any()))
+                        .thenReturn(Optional.of("mitigation-not-in-check-mitigation"));
+
+                // Act
+                var result =
+                        eventResolver.resolve(
+                                basicEventWithCheckMitigationConfigured,
+                                new EventResolveParameters(
+                                        "journeyContext", ipvSessionItem, clientOAuthSessionItem));
+
+                // Assert
+                assertEquals(originalTargetStateObj, result.state());
+            }
+
+            @Test
+            void resolveShouldReturnTargetStateIfNoValidMitigationFoundInSecurityCheckCredential()
+                    throws Exception {
+                // Arrange
+                var basicEventWithCheckMitigationConfigured = new BasicEvent();
+                var originalTargetStateObj = new BasicState();
+                basicEventWithCheckMitigationConfigured.setTargetStateObj(originalTargetStateObj);
+
+                LinkedHashMap<String, Event> checkMitigation = new LinkedHashMap<>();
+
+                checkMitigation.put("first-mitigation", new BasicEvent());
+
+                basicEventWithCheckMitigationConfigured.setCheckMitigation(checkMitigation);
+
+                when(mockCimitUtilityService.getContraIndicatorsFromVc(
+                                SIGNED_CONTRA_INDICATOR_VC_1, clientOAuthSessionItem.getUserId()))
+                        .thenReturn(List.of());
+                when(mockCimitUtilityService.getMitigationJourneyEvent(eq(List.of()), any()))
+                        .thenReturn(Optional.empty());
+
+                // Act
+                var result =
+                        eventResolver.resolve(
+                                basicEventWithCheckMitigationConfigured,
+                                new EventResolveParameters(
+                                        "journeyContext", ipvSessionItem, clientOAuthSessionItem));
+
+                // Assert
+                assertEquals(originalTargetStateObj, result.state());
+            }
+
+            @Test
+            void
+                    resolveReturnsOriginalTargetStateIfOnReverificationJourneyAndCheckMitigationIsConfigured()
+                            throws Exception {
+                var basicEventWithCheckMitigationConfigured = new BasicEvent();
+                var originalTargetStateObj = new BasicState();
+                basicEventWithCheckMitigationConfigured.setTargetStateObj(originalTargetStateObj);
+
+                LinkedHashMap<String, Event> checkMitigation = new LinkedHashMap<>();
+
+                checkMitigation.put("first-mitigation", new BasicEvent());
+
+                basicEventWithCheckMitigationConfigured.setCheckMitigation(checkMitigation);
+
+                // Act
+                var result =
+                        eventResolver.resolve(
+                                basicEventWithCheckMitigationConfigured,
+                                new EventResolveParameters(
+                                        "journeyContext",
+                                        ipvSessionItem,
+                                        ClientOAuthSessionItem.builder()
+                                                .scope(ScopeConstants.REVERIFICATION)
+                                                .build()));
+
+                // Assert
+                assertEquals(originalTargetStateObj, result.state());
+                verify(mockCimitUtilityService, times(0)).getContraIndicatorsFromVc(any(), any());
+            }
+
+            @Test
+            void resolveShouldThrowIfIpvSessionItemDoesNotContainSecurityCheckCredential() {
+                // Arrange
+                var basicEventWithCheckMitigationConfigured = new BasicEvent();
+                LinkedHashMap<String, Event> checkMitigation = new LinkedHashMap<>();
+                checkMitigation.put("first-mitigation", new BasicEvent());
+                basicEventWithCheckMitigationConfigured.setCheckMitigation(checkMitigation);
+
+                var ipvSessionWithMissingSecurityCheckCredential = new IpvSessionItem();
+
+                // Act/Assert
+                var exception =
+                        assertThrows(
+                                JourneyEngineException.class,
+                                () ->
+                                        eventResolver.resolve(
+                                                basicEventWithCheckMitigationConfigured,
+                                                new EventResolveParameters(
+                                                        "journeyContext",
+                                                        ipvSessionWithMissingSecurityCheckCredential,
+                                                        clientOAuthSessionItem)));
+
+                assertEquals(
+                        "Missing security check credential", exception.getCause().getMessage());
+            }
+        }
+    }
+
+    @Nested
+    class ExitNestedJourneyEventTest {
+        @BeforeEach
+        void setUp() {
+            clientOAuthSessionItem = new ClientOAuthSessionItem();
+            eventResolveParameters =
+                    new EventResolveParameters(
+                            "journeyContext", new IpvSessionItem(), clientOAuthSessionItem);
+        }
+
+        @Test
+        void resolveShouldResolveEventFromNestedJourneyExitEvents() throws Exception {
+            var expectedResult = new BasicState();
+            expectedResult.setName("target-state");
+
+            BasicEvent nestedJourneyExitEvent = new BasicEvent();
+            nestedJourneyExitEvent.setTargetStateObj(expectedResult);
+
+            ExitNestedJourneyEvent exitNestedJourneyEvent = new ExitNestedJourneyEvent();
+            exitNestedJourneyEvent.setExitEventToEmit("exiting");
+            exitNestedJourneyEvent.setNestedJourneyExitEvents(
+                    Map.of("exiting", nestedJourneyExitEvent));
+
+            assertEquals(
+                    expectedResult,
+                    eventResolver.resolve(exitNestedJourneyEvent, eventResolveParameters).state());
+        }
+
+        @Test
+        void resolveShouldThrowIfEventNotFoundInNestedJourneyExitEvents() {
+            ExitNestedJourneyEvent exitNestedJourneyEvent = new ExitNestedJourneyEvent();
+            exitNestedJourneyEvent.setNestedJourneyExitEvents(Map.of("exiting", new BasicEvent()));
+            exitNestedJourneyEvent.setExitEventToEmit("not-found");
+
+            assertThrows(
+                    UnknownEventException.class,
+                    () -> eventResolver.resolve(exitNestedJourneyEvent, eventResolveParameters));
+        }
+    }
+}
diff --git a/lambdas/process-journey-event/src/test/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/ExitNestedJourneyEventTest.java b/lambdas/process-journey-event/src/test/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/ExitNestedJourneyEventTest.java
index bec66bbc4c..a61f06ee4b 100644
--- a/lambdas/process-journey-event/src/test/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/ExitNestedJourneyEventTest.java
+++ b/lambdas/process-journey-event/src/test/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/ExitNestedJourneyEventTest.java
@@ -1,49 +1,13 @@
 package uk.gov.di.ipv.core.processjourneyevent.statemachine.events;
 
 import org.junit.jupiter.api.Test;
-import uk.gov.di.ipv.core.library.service.ConfigService;
-import uk.gov.di.ipv.core.processjourneyevent.statemachine.TransitionResult;
-import uk.gov.di.ipv.core.processjourneyevent.statemachine.exceptions.UnknownEventException;
-import uk.gov.di.ipv.core.processjourneyevent.statemachine.states.BasicState;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.states.State;
-import uk.gov.di.ipv.core.processjourneyevent.statemachine.stepresponses.JourneyContext;
 
 import java.util.Map;
 
-import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
 
 class ExitNestedJourneyEventTest {
-    private static final JourneyContext JOURNEY_CONTEXT =
-            new JourneyContext(mock(ConfigService.class), "");
-
-    @Test
-    void resolveShouldResolveEventFromNestedJourneyExitEvents() throws UnknownEventException {
-        var expectedResult = new TransitionResult(new BasicState());
-        BasicEvent nestedJourneyExitEvent = mock(BasicEvent.class);
-        when(nestedJourneyExitEvent.resolve(any(JourneyContext.class))).thenReturn(expectedResult);
-
-        ExitNestedJourneyEvent exitNestedJourneyEvent = new ExitNestedJourneyEvent();
-        exitNestedJourneyEvent.setExitEventToEmit("exiting");
-        exitNestedJourneyEvent.setNestedJourneyExitEvents(
-                Map.of("exiting", nestedJourneyExitEvent));
-
-        assertEquals(expectedResult, exitNestedJourneyEvent.resolve(JOURNEY_CONTEXT));
-    }
-
-    @Test
-    void resolveShouldThrowIfEventNotFoundInNestedJourneyExitEvents() {
-        ExitNestedJourneyEvent exitNestedJourneyEvent = new ExitNestedJourneyEvent();
-        exitNestedJourneyEvent.setNestedJourneyExitEvents(Map.of("exiting", new BasicEvent()));
-        exitNestedJourneyEvent.setExitEventToEmit("not-found");
-
-        assertThrows(
-                UnknownEventException.class, () -> exitNestedJourneyEvent.resolve(JOURNEY_CONTEXT));
-    }
-
     @Test
     void initializeShouldThrowAnUnsupportedOperationException() {
         ExitNestedJourneyEvent exitNestedJourneyEvent = new ExitNestedJourneyEvent();
diff --git a/lambdas/process-journey-event/src/test/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/states/BasicStateTest.java b/lambdas/process-journey-event/src/test/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/states/BasicStateTest.java
index 163f0c2582..34340294f9 100644
--- a/lambdas/process-journey-event/src/test/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/states/BasicStateTest.java
+++ b/lambdas/process-journey-event/src/test/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/states/BasicStateTest.java
@@ -1,25 +1,42 @@
 package uk.gov.di.ipv.core.processjourneyevent.statemachine.states;
 
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
+import uk.gov.di.ipv.core.library.domain.ScopeConstants;
+import uk.gov.di.ipv.core.library.persistence.item.ClientOAuthSessionItem;
+import uk.gov.di.ipv.core.library.persistence.item.IpvSessionItem;
+import uk.gov.di.ipv.core.library.service.CimitUtilityService;
 import uk.gov.di.ipv.core.library.service.ConfigService;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.events.BasicEvent;
+import uk.gov.di.ipv.core.processjourneyevent.statemachine.events.EventResolveParameters;
+import uk.gov.di.ipv.core.processjourneyevent.statemachine.events.EventResolver;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.exceptions.UnknownEventException;
-import uk.gov.di.ipv.core.processjourneyevent.statemachine.stepresponses.JourneyContext;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.stepresponses.PageStepResponse;
 
 import java.util.Map;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
 
 @ExtendWith(MockitoExtension.class)
 class BasicStateTest {
-    @Mock private ConfigService mockConfigService;
-    @InjectMocks private JourneyContext journeyContext;
+    private EventResolveParameters eventResolveParameters;
+    private EventResolver eventResolver;
+
+    @BeforeEach
+    void setUp() {
+        eventResolveParameters =
+                new EventResolveParameters(
+                        "journeyContext",
+                        new IpvSessionItem(),
+                        ClientOAuthSessionItem.builder().scope(ScopeConstants.OPENID).build());
+
+        eventResolver =
+                new EventResolver(mock(CimitUtilityService.class), mock(ConfigService.class));
+    }
 
     @Test
     void transitionShouldReturnAStateWithAResponse() throws Exception {
@@ -32,7 +49,9 @@ void transitionShouldReturnAStateWithAResponse() throws Exception {
         currentToTargetEvent.setTargetStateObj(targetState);
         currentState.setEvents(Map.of("next", currentToTargetEvent));
 
-        var result = currentState.transition("next", "startState", journeyContext);
+        var result =
+                currentState.transition(
+                        "next", "startState", eventResolveParameters, eventResolver);
 
         assertEquals(targetState, result.state());
     }
@@ -49,7 +68,9 @@ void transitionShouldUseEventsFromParentState() throws Exception {
         BasicState currentState = new BasicState();
         currentState.setParentObj(parentState);
 
-        var result = currentState.transition("parent-event", "startState", journeyContext);
+        var result =
+                currentState.transition(
+                        "parent-event", "startState", eventResolveParameters, eventResolver);
 
         assertEquals(parentEventTargetState, result.state());
     }
@@ -59,14 +80,26 @@ void transitionShouldReturnThisIfAttemptRecoveryEventReceived() throws Exception
         var state = new BasicState();
 
         assertEquals(
-                state, state.transition("attempt-recovery", "startState", journeyContext).state());
+                state,
+                state.transition(
+                                "attempt-recovery",
+                                "startState",
+                                eventResolveParameters,
+                                eventResolver)
+                        .state());
     }
 
     @Test
     void transitionShouldThrowIfEventNotFound() {
         assertThrows(
                 UnknownEventException.class,
-                () -> new BasicState().transition("unknown-event", "startState", journeyContext));
+                () ->
+                        new BasicState()
+                                .transition(
+                                        "unknown-event",
+                                        "startState",
+                                        eventResolveParameters,
+                                        eventResolver));
     }
 
     @Test
diff --git a/lambdas/process-journey-event/src/test/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/states/NestedJourneyInvokeStateTest.java b/lambdas/process-journey-event/src/test/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/states/NestedJourneyInvokeStateTest.java
index 981ce96bf4..00fa7f95b9 100644
--- a/lambdas/process-journey-event/src/test/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/states/NestedJourneyInvokeStateTest.java
+++ b/lambdas/process-journey-event/src/test/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/states/NestedJourneyInvokeStateTest.java
@@ -1,12 +1,17 @@
 package uk.gov.di.ipv.core.processjourneyevent.statemachine.states;
 
 import org.junit.jupiter.api.Test;
-import uk.gov.di.ipv.core.library.service.ConfigService;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import uk.gov.di.ipv.core.library.persistence.item.ClientOAuthSessionItem;
+import uk.gov.di.ipv.core.library.persistence.item.IpvSessionItem;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.TransitionResult;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.events.BasicEvent;
+import uk.gov.di.ipv.core.processjourneyevent.statemachine.events.EventResolveParameters;
+import uk.gov.di.ipv.core.processjourneyevent.statemachine.events.EventResolver;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.exceptions.UnknownEventException;
 import uk.gov.di.ipv.core.processjourneyevent.statemachine.exceptions.UnknownStateException;
-import uk.gov.di.ipv.core.processjourneyevent.statemachine.stepresponses.JourneyContext;
 
 import java.util.Map;
 
@@ -17,15 +22,19 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
+@ExtendWith(MockitoExtension.class)
 class NestedJourneyInvokeStateTest {
-    private static final JourneyContext JOURNEY_CONTEXT =
-            new JourneyContext(mock(ConfigService.class), "");
+    private static final EventResolveParameters EVENT_RESOLVE_PARAMETERS =
+            new EventResolveParameters(
+                    "journeyContext", new IpvSessionItem(), new ClientOAuthSessionItem());
+    @Mock private EventResolver eventResolver;
 
     @Test
     void transitionShouldUseEntryEventsWhenStartStateHasOnePart() throws Exception {
         var expectedResult = new TransitionResult(new BasicState());
         BasicEvent basicEvent = mock(BasicEvent.class);
-        when(basicEvent.resolve(any(JourneyContext.class))).thenReturn(expectedResult);
+        when(eventResolver.resolve(eq(basicEvent), any(EventResolveParameters.class)))
+                .thenReturn(expectedResult);
 
         NestedJourneyDefinition nestedJourneyDefinition = new NestedJourneyDefinition();
         nestedJourneyDefinition.setEntryEvents(Map.of("next", basicEvent));
@@ -34,7 +43,8 @@ void transitionShouldUseEntryEventsWhenStartStateHasOnePart() throws Exception {
         nestedJourneyInvokeState.setNestedJourneyDefinition(nestedJourneyDefinition);
 
         var actualResult =
-                nestedJourneyInvokeState.transition("next", "INVOKE_STATE", JOURNEY_CONTEXT);
+                nestedJourneyInvokeState.transition(
+                        "next", "INVOKE_STATE", EVENT_RESOLVE_PARAMETERS, eventResolver);
 
         assertEquals(expectedResult, actualResult);
     }
@@ -48,7 +58,10 @@ void transitionShouldReturnStateFromNestedJourneyDefinitionIfStartStateHasMultip
 
         var expectedResult = new TransitionResult(new BasicState());
         when(currentNestedState.transition(
-                        eq("next"), eq("NESTED_STATE"), any(JourneyContext.class)))
+                        eq("next"),
+                        eq("NESTED_STATE"),
+                        any(EventResolveParameters.class),
+                        eq(eventResolver)))
                 .thenReturn(expectedResult);
 
         NestedJourneyInvokeState nestedJourneyInvokeState = new NestedJourneyInvokeState();
@@ -56,7 +69,10 @@ void transitionShouldReturnStateFromNestedJourneyDefinitionIfStartStateHasMultip
 
         var actualResult =
                 nestedJourneyInvokeState.transition(
-                        "next", "INVOKE_STATE/NESTED_STATE", JOURNEY_CONTEXT);
+                        "next",
+                        "INVOKE_STATE/NESTED_STATE",
+                        EVENT_RESOLVE_PARAMETERS,
+                        eventResolver);
 
         assertEquals(expectedResult, actualResult);
     }
@@ -74,15 +90,24 @@ void transitionShouldHandleANestedNestedJourneyInvokeState() throws Exception {
                 mock(NestedJourneyInvokeState.class);
         var expectedResult = new TransitionResult(new BasicState());
         when(nestedNestedJourneyInvokeState.transition(
-                        eq("next"), eq("NESTED_STATE"), any(JourneyContext.class)))
+                        eq("next"),
+                        eq("NESTED_STATE"),
+                        any(EventResolveParameters.class),
+                        eq(eventResolver)))
                 .thenReturn(expectedResult);
         when(currentNestedState.transition(
-                        eq("next"), eq("NESTED_STATE"), any(JourneyContext.class)))
+                        eq("next"),
+                        eq("NESTED_STATE"),
+                        any(EventResolveParameters.class),
+                        eq(eventResolver)))
                 .thenReturn(new TransitionResult(nestedNestedJourneyInvokeState));
 
         var actualResult =
                 nestedJourneyInvokeState.transition(
-                        "next", "INVOKE_STATE/NESTED_STATE", JOURNEY_CONTEXT);
+                        "next",
+                        "INVOKE_STATE/NESTED_STATE",
+                        EVENT_RESOLVE_PARAMETERS,
+                        eventResolver);
 
         assertEquals(expectedResult, actualResult);
     }
@@ -99,7 +124,10 @@ void transitionShouldThrowIfUnknownEntryEvent() {
                 UnknownEventException.class,
                 () ->
                         nestedJourneyInvokeState.transition(
-                                "unknown", "INVOKE_STATE", JOURNEY_CONTEXT));
+                                "unknown",
+                                "INVOKE_STATE",
+                                EVENT_RESOLVE_PARAMETERS,
+                                eventResolver));
     }
 
     @Test
@@ -115,7 +143,10 @@ void transitionShouldThrowIfNestedStateNotFoundInDefinition() {
                 UnknownStateException.class,
                 () ->
                         nestedJourneyInvokeState.transition(
-                                "unknown", "INVOKE_STATE/UNKNOWN_STATE", JOURNEY_CONTEXT));
+                                "unknown",
+                                "INVOKE_STATE/UNKNOWN_STATE",
+                                EVENT_RESOLVE_PARAMETERS,
+                                eventResolver));
     }
 
     @Test
diff --git a/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/service/CimitUtilityService.java b/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/service/CimitUtilityService.java
index 1637093922..3920b4ad3d 100644
--- a/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/service/CimitUtilityService.java
+++ b/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/service/CimitUtilityService.java
@@ -106,6 +106,35 @@ private boolean isScoreBreachingCiThreshold(int score, Vot vot) {
                 > Integer.parseInt(configService.getParameter(CI_SCORING_THRESHOLD, vot.name()));
     }
 
+    public Optional<String> getMitigationJourneyEvent(
+            List<ContraIndicator> cis, Vot confidenceRequested) throws ConfigException {
+        var journeyResponse = getMitigationJourneyIfActive(cis, confidenceRequested);
+        if (journeyResponse.isPresent()) {
+            var journey = journeyResponse.get().getJourney();
+            return Optional.of(journey.substring(journey.lastIndexOf("/") + 1));
+        }
+        return Optional.empty();
+    }
+
+    private Optional<JourneyResponse> getMitigationJourneyIfActive(
+            List<ContraIndicator> cis, Vot confidenceRequested) throws ConfigException {
+        if (isBreachingCiThreshold(cis, confidenceRequested)) {
+            return Optional.of(
+                    getCiMitigationJourneyResponse(cis, confidenceRequested)
+                            .orElse(JOURNEY_FAIL_WITH_CI));
+        } else {
+            var mitigatedCi = hasMitigatedContraIndicator(cis);
+            if (mitigatedCi.isPresent()) {
+                var cimitConfig = configService.getCimitConfig();
+
+                return getMitigationJourneyResponse(
+                        cimitConfig.get(mitigatedCi.get().getCode()),
+                        mitigatedCi.get().getDocument());
+            }
+        }
+        return Optional.empty();
+    }
+
     public Optional<JourneyResponse> getMitigationJourneyIfBreaching(
             List<ContraIndicator> cis, Vot confidenceRequested) throws ConfigException {
         if (isBreachingCiThreshold(cis, confidenceRequested)) {
diff --git a/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/service/ClientOAuthSessionDetailsService.java b/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/service/ClientOAuthSessionDetailsService.java
index 9075fce7ab..c792c94835 100644
--- a/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/service/ClientOAuthSessionDetailsService.java
+++ b/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/service/ClientOAuthSessionDetailsService.java
@@ -50,7 +50,6 @@ public ClientOAuthSessionItem generateClientSessionDetails(
         clientOAuthSessionItem.setRedirectUri(claimsSet.getStringClaim("redirect_uri"));
         clientOAuthSessionItem.setState(claimsSet.getStringClaim("state"));
         clientOAuthSessionItem.setUserId(claimsSet.getSubject());
-        clientOAuthSessionItem.setScope(claimsSet.getStringClaim("scope"));
         clientOAuthSessionItem.setGovukSigninJourneyId(
                 claimsSet.getStringClaim("govuk_signin_journey_id"));
         clientOAuthSessionItem.setVtr(claimsSet.getStringListClaim("vtr"));
diff --git a/libs/common-services/src/test/java/uk/gov/di/ipv/core/library/service/CimitUtilityServiceTest.java b/libs/common-services/src/test/java/uk/gov/di/ipv/core/library/service/CimitUtilityServiceTest.java
index 9d100eb904..9bdcc314af 100644
--- a/libs/common-services/src/test/java/uk/gov/di/ipv/core/library/service/CimitUtilityServiceTest.java
+++ b/libs/common-services/src/test/java/uk/gov/di/ipv/core/library/service/CimitUtilityServiceTest.java
@@ -636,6 +636,73 @@ void getContraIndicatorsFromVcThrowsErrorIfInvalidVc() {
                                 VC_RESIDENCE_PERMIT_DCMAW, "mock-user-id"));
     }
 
+    @Test
+    void getMitigationJourneyEventReturnsBreachingMitigationJourneyEvent() throws Exception {
+        // Arrange
+        var code = "ci_code";
+        var journey = "some_mitigation";
+        String documentType = "passport";
+        var ci = createCi(code);
+        ci.setDocument("passport/some-document");
+
+        when(mockConfigService.getCimitConfig())
+                .thenReturn(Map.of(code, List.of(new MitigationRoute(journey, documentType))));
+
+        Map<String, ContraIndicatorConfig> ciConfigMap =
+                Map.of(code, new ContraIndicatorConfig(code, 7, -5, "X"));
+
+        when(mockConfigService.getContraIndicatorConfigMap()).thenReturn(ciConfigMap);
+        when(mockConfigService.getParameter(CI_SCORING_THRESHOLD, TEST_VOT.name())).thenReturn("5");
+
+        // act
+        var result = cimitUtilityService.getMitigationJourneyEvent(List.of(ci), TEST_VOT);
+
+        // assert
+        assertEquals(Optional.of(journey), result);
+    }
+
+    @Test
+    void getMitigationJourneyEventReturnsMitigationJourneyEventIfNotBreachingButCiIsMitigated()
+            throws Exception {
+        // Arrange
+        var code = "ci_code";
+        var journey = "some_mitigation";
+        String documentType = "passport";
+        var ci = createCi(code, BASE_TIME.minusSeconds(1), List.of(new Mitigation()), documentType);
+
+        when(mockConfigService.getCimitConfig())
+                .thenReturn(Map.of(code, List.of(new MitigationRoute(journey, documentType))));
+
+        Map<String, ContraIndicatorConfig> ciConfigMap =
+                Map.of(code, new ContraIndicatorConfig(code, 4, -3, "X"));
+
+        when(mockConfigService.getContraIndicatorConfigMap()).thenReturn(ciConfigMap);
+        when(mockConfigService.getParameter(CI_SCORING_THRESHOLD, TEST_VOT.name())).thenReturn("5");
+
+        // act
+        var result = cimitUtilityService.getMitigationJourneyEvent(List.of(ci), TEST_VOT);
+
+        // assert
+        assertEquals(Optional.of(journey), result);
+    }
+
+    @Test
+    void getMitigationJourneyEventReturnsEmptyIfNoCis() throws Exception {
+        // Arrange
+        var code = "ci_code";
+        Map<String, ContraIndicatorConfig> ciConfigMap =
+                Map.of(code, new ContraIndicatorConfig(code, 4, -3, "X"));
+        when(mockConfigService.getContraIndicatorConfigMap()).thenReturn(ciConfigMap);
+
+        when(mockConfigService.getParameter(CI_SCORING_THRESHOLD, TEST_VOT.name())).thenReturn("5");
+
+        // act
+        var result = cimitUtilityService.getMitigationJourneyEvent(List.of(), TEST_VOT);
+
+        // assert
+        assertEquals(Optional.empty(), result);
+    }
+
     private static ContraIndicator createCi(String code) {
         var ci = new ContraIndicator();
         ci.setCode(code);


Reviews:
user: login: thebauSoftwire
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T11:31:31Z
user: login: Joe-Edwards-GDS
body: Thanks, this looks good!

I've left a few suggestions, but they're mostly optional tidying up :)
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T13:41:07Z
user: login: thebauSoftwire
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T15:19:27Z
user: login: thebauSoftwire
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T15:29:04Z
user: login: Joe-Edwards-GDS
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T15:35:03Z
user: login: thebauSoftwire
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T16:07:13Z
user: login: thebauSoftwire
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T16:23:01Z
user: login: Joe-Edwards-GDS
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T16:50:04Z
user: login: thebauSoftwire
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T16:55:42Z
user: login: Joe-Edwards-GDS
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T17:03:37Z
user: login: thebauSoftwire
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T17:25:43Z
user: login: Joe-Edwards-GDS
body: One comment, but otherwise I think this looks good - assuming that we're leaving a refactor of the journey engine for later.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T14:44:10Z
user: login: Joe-Edwards-GDS
body: Looks good, but happy to review again after the refactor, if you think it's an improvement :)
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T08:54:46Z
user: login: Joe-Edwards-GDS
body: Thanks, this is neat 😄 

As discussed, we could probably go a lot further (e.g. to do the same thing at the state level), but I think we should save that for another day!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T10:20:03Z

Review comments:
diff_hunk: @@ -73,12 +84,53 @@ public TransitionResult resolve(JourneyContext journeyContext) throws UnknownEve
             if (firstFeatureFlag.isPresent()) {
                 String featureFlagValue = firstFeatureFlag.get();
                 LOGGER.info("Feature flag '{}' is set. Using alternative event", featureFlagValue);
-                return checkFeatureFlag.get(featureFlagValue).resolve(journeyContext);
+                return checkFeatureFlag.get(featureFlagValue).resolve(resolveParameters);
             }
         }
+        if (checkMitigation != null) {
+            var matchedMitigationEvent = getMitigationEvent(resolveParameters);
+
+            if (matchedMitigationEvent.isPresent()) {
+                var mitigationEvent = matchedMitigationEvent.get();
+                LOGGER.info("Mitigation found. Using alternative event.");
+                return mitigationEvent.resolve(resolveParameters);
path: lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/BasicEvent.java
user: login: thebauSoftwire
body: I tried a couple of ways to include the mitigation name in this log, the best I could come up with without increasing cognitive complexity was updating `getMitigationEvent` below to return an `Optional<String>` and moving `validMitigation.map(s -> checkMitigation.get(s));` in this method so that:
```
var validMitigation = getMitigationEvent(resolveParameters);
var matchedMitigationEvent = validMitigation.map(s -> checkMitigation.get(s));

if (matchedMitigationEvent.isPresent()) {
    var mitigationEvent = matchedMitigationEvent.get();
    LOGGER.info("Mitigation '{}' found. Using alternative event.", validMitigation.get());
    return mitigationEvent.resolve(resolveParameters);
end
```
Where we can safely call `validMitigation.get()` without an `isPresent()` as `matchedMitigationEvent.isPresent()` can only be true if there is a value for `validMitigation`.  We could also replace it with `validMitigation.orElseThrow()` but sonar wasn't happy with either.

So settled with removing it from this log instead of suppressing a sonar error
created_at: 2025-03-11T11:31:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,6 +231,35 @@ const renderTransitions = (journeyStates, formData) => {
   return { transitionsMermaid: stateTransitions.join("\n"), states };
 };
 
+const createTransitionLabel = ({
+  eventName,
+  targetEntryEvent,
+  journeyContext,
+  mitigation,
+}) => {
+  const eventLabel = `${eventName}${targetEntryEvent ? `/${targetEntryEvent}` : ""}`;
+  const journeyContextLabel = journeyContext
+    ? ` - journeyContext: ${journeyContext}`
+    : "";
+  const mitigationLabel = mitigation ? `CI mitigation: ${mitigation}` : "";
+
+  const labelWithClass = (className, eventTransitionLabel) =>
+    `<span class="${className}">${eventTransitionLabel}</span>`;
+
+  if (journeyContext) {
+    return labelWithClass(
+      JOURNEY_CONTEXT_TRANSITION_CLASSNAME,
+      eventLabel + journeyContextLabel,
+    );
+  }
+
+  if (mitigationLabel) {
+    return labelWithClass(MITIGATIONS_TRANSITION_CLASSNAME, mitigationLabel);
path: journey-map/public/render.mjs
user: login: Joe-Edwards-GDS
body: Do you think there's any value in having the original event as well? I'm not sure - I like keeping it as concise as possible, so long as there's no ambiguity.
created_at: 2025-03-11T11:40:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -106,6 +106,16 @@ private boolean isScoreBreachingCiThreshold(int score, Vot vot) {
                 > Integer.parseInt(configService.getParameter(CI_SCORING_THRESHOLD, vot.name()));
     }
 
+    public Optional<String> getMitigationJourneyEvent(
+            List<ContraIndicator> cis, Vot confidenceRequested) throws ConfigException {
+        var journeyResponse = getMitigationJourneyIfBreaching(cis, confidenceRequested);
+        if (journeyResponse.isPresent()) {
+            var journey = journeyResponse.get().getJourney();
+            return Optional.of(journey.substring(journey.lastIndexOf("/") + 1));
path: libs/common-services/src/main/java/uk/gov/di/ipv/core/library/service/CimitUtilityService.java
user: login: Joe-Edwards-GDS
body: (A bit annoying that we have to do this string manipulation - this is kinda related to https://govukverify.atlassian.net/browse/PYIC-3927 but we're unlikely to do anything about it any time soon..)
created_at: 2025-03-11T11:44:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -545,4 +593,12 @@ private boolean isBackEventDefinedOnState(JourneyState journeyState) {
                         instanceof BasicState basicState
                 && basicState.getEvents().containsKey(BACK_EVENT);
     }
+
+    private void logErrorWithJourneyEventAndJourneyType(
path: lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/ProcessJourneyEventHandler.java
user: login: Joe-Edwards-GDS
body: Should this include the state as well as the event and journey?
created_at: 2025-03-11T11:52:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -277,21 +287,53 @@ private StepResponse executeJourneyEvent(
                                     ipvSessionItem.getState().state()));
             throw new JourneyEngineException();
         } catch (UnknownEventException e) {
-            LOGGER.error(
-                    LogHelper.buildErrorMessage(
-                                    "Invalid journey event provided, failed to execute journey engine step.",
-                                    e)
-                            .with(LOG_JOURNEY_EVENT.getFieldName(), journeyEvent));
+            logErrorWithJourneyEventAndJourneyType(
+                    "Invalid journey event provided, failed to execute journey engine step.",
+                    e,
+                    journeyEvent,
+                    ipvSessionItem.getState().subJourney().name());
             throw new JourneyEngineException();
         } catch (StateMachineNotFoundException e) {
-            LOGGER.error(
-                    LogHelper.buildErrorMessage(
-                                    "State machine not found for journey type, failed to execute journey engine step",
-                                    e)
-                            .with(LOG_JOURNEY_EVENT.getFieldName(), journeyEvent)
-                            .with(
-                                    LOG_JOURNEY_TYPE.getFieldName(),
-                                    ipvSessionItem.getState().subJourney().name()));
+            logErrorWithJourneyEventAndJourneyType(
+                    "State machine not found for journey type, failed to execute journey engine step",
+                    e,
+                    journeyEvent,
+                    ipvSessionItem.getState().subJourney().name());
+            throw new JourneyEngineException();
+        } catch (MissingSecurityCheckCredential e) {
+            logErrorWithJourneyEventAndJourneyType(
+                    "Missing security check credential from session.",
+                    e,
+                    journeyEvent,
+                    ipvSessionItem.getState().subJourney().name());
+            throw new JourneyEngineException();
path: lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/ProcessJourneyEventHandler.java
user: login: Joe-Edwards-GDS
body: Our exception handling is really a bit suspect 🤔

Feels like we need a shared exception base class that contains all the properties we need - i.e. a 'log message' as well as the detail message. Perhaps linked to `ErrorResponse` as well. That way all these catch blocks could be combined into a single one that just logs the exception with some extra detail.

That's way out of scope though, so no need to worry for this PR.
created_at: 2025-03-11T11:58:52Z
author_association: CONTRIBUTOR
path: lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/EventResolveParameters.java
user: login: Joe-Edwards-GDS
body: The `EventResolveParameters` abstraction here feels a little bit weird, because it includes a service (which is not really a 'parameter'). That was kinda already true with the `configService` inside the journey context, so not really a change.

It feels ok to leave, and I can't think an obviously better name (without overloading 'context' even further...)

If you want to explore a slightly more involved alternative, you could try inverting the relationship, and make this an `EventResolver` class, something like:

```
public class EventResolver {
  private CimitUtilityService cimitUtilityService;
  // etc.

  public TransitionResult resolve(Event event) {
    if (<is disabled>) {
      return resolve(<get disabled event>);
    }
    // etc.
    return new TransitionResult(...);
  }
}
```
I haven't thought through the full implications, but it could be a more natural way of doing it?
created_at: 2025-03-11T12:21:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,12 @@
+package uk.gov.di.ipv.core.processjourneyevent.statemachine.events;
+
+import uk.gov.di.ipv.core.library.persistence.item.ClientOAuthSessionItem;
+import uk.gov.di.ipv.core.library.persistence.item.IpvSessionItem;
+import uk.gov.di.ipv.core.library.service.CimitUtilityService;
+import uk.gov.di.ipv.core.processjourneyevent.statemachine.stepresponses.JourneyContext;
+
+public record EventResolveParameters(
+        JourneyContext journeyContext,
path: lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/EventResolveParameters.java
user: login: Joe-Edwards-GDS
body: Should we flatten this out, so there is a `String journeyContext` and a `ConfigService configService`?

(This 'journey context' actually predates the current 'journeyContext', and was meant to be a general container for stuff the journey engine needed to resolve events - as you've created!)
created_at: 2025-03-11T12:23:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -277,21 +287,53 @@ private StepResponse executeJourneyEvent(
                                     ipvSessionItem.getState().state()));
             throw new JourneyEngineException();
         } catch (UnknownEventException e) {
-            LOGGER.error(
-                    LogHelper.buildErrorMessage(
-                                    "Invalid journey event provided, failed to execute journey engine step.",
-                                    e)
-                            .with(LOG_JOURNEY_EVENT.getFieldName(), journeyEvent));
+            logErrorWithJourneyEventAndJourneyType(
+                    "Invalid journey event provided, failed to execute journey engine step.",
+                    e,
+                    journeyEvent,
+                    ipvSessionItem.getState().subJourney().name());
             throw new JourneyEngineException();
         } catch (StateMachineNotFoundException e) {
-            LOGGER.error(
-                    LogHelper.buildErrorMessage(
-                                    "State machine not found for journey type, failed to execute journey engine step",
-                                    e)
-                            .with(LOG_JOURNEY_EVENT.getFieldName(), journeyEvent)
-                            .with(
-                                    LOG_JOURNEY_TYPE.getFieldName(),
-                                    ipvSessionItem.getState().subJourney().name()));
+            logErrorWithJourneyEventAndJourneyType(
+                    "State machine not found for journey type, failed to execute journey engine step",
+                    e,
+                    journeyEvent,
+                    ipvSessionItem.getState().subJourney().name());
+            throw new JourneyEngineException();
+        } catch (MissingSecurityCheckCredential e) {
+            logErrorWithJourneyEventAndJourneyType(
+                    "Missing security check credential from session.",
+                    e,
+                    journeyEvent,
+                    ipvSessionItem.getState().subJourney().name());
+            throw new JourneyEngineException();
path: lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/ProcessJourneyEventHandler.java
user: login: Joe-Edwards-GDS
body: If these are all thrown from the same place (the `resolve` method), then one option would be to catch there and rethrow a `JourneyEngineException` with the original exception as a cause, to avoid propagating all the checked exceptions. Fine to leave as-is.
created_at: 2025-03-11T12:32:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -73,12 +84,53 @@ public TransitionResult resolve(JourneyContext journeyContext) throws UnknownEve
             if (firstFeatureFlag.isPresent()) {
                 String featureFlagValue = firstFeatureFlag.get();
                 LOGGER.info("Feature flag '{}' is set. Using alternative event", featureFlagValue);
-                return checkFeatureFlag.get(featureFlagValue).resolve(journeyContext);
+                return checkFeatureFlag.get(featureFlagValue).resolve(resolveParameters);
             }
         }
+        if (checkMitigation != null) {
+            var matchedMitigationEvent = getMitigationEvent(resolveParameters);
+
+            if (matchedMitigationEvent.isPresent()) {
+                var mitigationEvent = matchedMitigationEvent.get();
+                LOGGER.info("Mitigation found. Using alternative event.");
+                return mitigationEvent.resolve(resolveParameters);
path: lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/BasicEvent.java
user: login: Joe-Edwards-GDS
body: Hmm, I see the problem - Sonar doesn't understand the relationship between the two optionals.

I'd be inclined to do almost what you describe, but with the map access explicit:
```
var mitigation = getMitigation(resolveParameters);

if (mitigation.isPresent() && checkMitigation.containsKey(mitigation.get())) {
    var mitigationEvent = checkMitigation.get(mitigation.get());
    LOGGER.info("Mitigation '{}' found. Using alternative event.", mitigation.get());
    return mitigationEvent.resolve(resolveParameters);
}
```
created_at: 2025-03-11T12:40:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -73,12 +84,53 @@ public TransitionResult resolve(JourneyContext journeyContext) throws UnknownEve
             if (firstFeatureFlag.isPresent()) {
                 String featureFlagValue = firstFeatureFlag.get();
                 LOGGER.info("Feature flag '{}' is set. Using alternative event", featureFlagValue);
-                return checkFeatureFlag.get(featureFlagValue).resolve(journeyContext);
+                return checkFeatureFlag.get(featureFlagValue).resolve(resolveParameters);
             }
         }
+        if (checkMitigation != null) {
+            var matchedMitigationEvent = getMitigationEvent(resolveParameters);
+
+            if (matchedMitigationEvent.isPresent()) {
+                var mitigationEvent = matchedMitigationEvent.get();
+                LOGGER.info("Mitigation found. Using alternative event.");
+                return mitigationEvent.resolve(resolveParameters);
+            }
+        }
+
         return new TransitionResult(targetStateObj, auditEvents, auditContext, targetEntryEvent);
     }
 
+    private Optional<Event> getMitigationEvent(EventResolveParameters resolveParameters)
+            throws MissingSecurityCheckCredential, CiExtractionException, CredentialParseException,
+                    ParseException, ConfigException {
+        var ipvSessionItem = resolveParameters.ipvSessionItem();
+        var clientOAuthSessionItem = resolveParameters.clientOAuthSessionItem();
+        var cimitUtilityService = resolveParameters.cimitUtilityService();
+
+        var securityCheckCredential = ipvSessionItem.getSecurityCheckCredential();
+
+        if (StringUtils.isEmpty(securityCheckCredential)) {
+            LOGGER.error(LogHelper.buildErrorMessage(MISSING_SECURITY_CHECK_CREDENTIAL));
+            throw new MissingSecurityCheckCredential("Missing security check credential");
+        }
+
+        var contraIndicators =
+                cimitUtilityService.getContraIndicatorsFromVc(
+                        securityCheckCredential, clientOAuthSessionItem.getUserId());
+
+        var lowestGpg45ConfidenceRequested =
+                clientOAuthSessionItem
+                        .getParsedVtr()
+                        .getLowestStrengthRequestedGpg45Vot(
+                                resolveParameters.journeyContext().configService());
path: lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/BasicEvent.java
user: login: Joe-Edwards-GDS
body: Elsewhere (e.g. `CriCheckingService`) we're using `ipvSession.getTargetVot()` for this. They should generally be equivalent, but I think there's an edge case somewhere (@DanCorderIPV do you recall?)
created_at: 2025-03-11T13:36:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -32,8 +38,13 @@ public class BasicEvent implements Event {
     private LinkedHashMap<String, Event> checkJourneyContext;
     private List<AuditEventTypes> auditEvents;
     private LinkedHashMap<String, String> auditContext;
+    private LinkedHashMap<String, Event> checkMitigation;
path: lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/BasicEvent.java
user: login: Joe-Edwards-GDS
body: Do we need to update `JourneyMapTest` to cover these events (e.g. in `checkTargetStatesExist` and other places that read into the conditional events)
created_at: 2025-03-11T13:38:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,6 +231,35 @@ const renderTransitions = (journeyStates, formData) => {
   return { transitionsMermaid: stateTransitions.join("\n"), states };
 };
 
+const createTransitionLabel = ({
+  eventName,
+  targetEntryEvent,
+  journeyContext,
+  mitigation,
+}) => {
+  const eventLabel = `${eventName}${targetEntryEvent ? `/${targetEntryEvent}` : ""}`;
+  const journeyContextLabel = journeyContext
+    ? ` - journeyContext: ${journeyContext}`
+    : "";
+  const mitigationLabel = mitigation ? `CI mitigation: ${mitigation}` : "";
+
+  const labelWithClass = (className, eventTransitionLabel) =>
+    `<span class="${className}">${eventTransitionLabel}</span>`;
+
+  if (journeyContext) {
+    return labelWithClass(
+      JOURNEY_CONTEXT_TRANSITION_CLASSNAME,
+      eventLabel + journeyContextLabel,
+    );
+  }
+
+  if (mitigationLabel) {
+    return labelWithClass(MITIGATIONS_TRANSITION_CLASSNAME, mitigationLabel);
path: journey-map/public/render.mjs
user: login: thebauSoftwire
body: I assumed that the original event would always be `fail-with-ci` (can't think of a case where it wouldn't be) so decided to omit it from the label
created_at: 2025-03-11T15:19:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -73,12 +84,53 @@ public TransitionResult resolve(JourneyContext journeyContext) throws UnknownEve
             if (firstFeatureFlag.isPresent()) {
                 String featureFlagValue = firstFeatureFlag.get();
                 LOGGER.info("Feature flag '{}' is set. Using alternative event", featureFlagValue);
-                return checkFeatureFlag.get(featureFlagValue).resolve(journeyContext);
+                return checkFeatureFlag.get(featureFlagValue).resolve(resolveParameters);
             }
         }
+        if (checkMitigation != null) {
+            var matchedMitigationEvent = getMitigationEvent(resolveParameters);
+
+            if (matchedMitigationEvent.isPresent()) {
+                var mitigationEvent = matchedMitigationEvent.get();
+                LOGGER.info("Mitigation found. Using alternative event.");
+                return mitigationEvent.resolve(resolveParameters);
path: lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/BasicEvent.java
user: login: thebauSoftwire
body: Thanks for this!! 🙏 Moved `checkMitigation.containsKey(mitigation.get())` into `getMitigationEvent` below to keep cognitive complexity below the threshold
created_at: 2025-03-11T15:29:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,6 +231,35 @@ const renderTransitions = (journeyStates, formData) => {
   return { transitionsMermaid: stateTransitions.join("\n"), states };
 };
 
+const createTransitionLabel = ({
+  eventName,
+  targetEntryEvent,
+  journeyContext,
+  mitigation,
+}) => {
+  const eventLabel = `${eventName}${targetEntryEvent ? `/${targetEntryEvent}` : ""}`;
+  const journeyContextLabel = journeyContext
+    ? ` - journeyContext: ${journeyContext}`
+    : "";
+  const mitigationLabel = mitigation ? `CI mitigation: ${mitigation}` : "";
+
+  const labelWithClass = (className, eventTransitionLabel) =>
+    `<span class="${className}">${eventTransitionLabel}</span>`;
+
+  if (journeyContext) {
+    return labelWithClass(
+      JOURNEY_CONTEXT_TRANSITION_CLASSNAME,
+      eventLabel + journeyContextLabel,
+    );
+  }
+
+  if (mitigationLabel) {
+    return labelWithClass(MITIGATIONS_TRANSITION_CLASSNAME, mitigationLabel);
path: journey-map/public/render.mjs
user: login: Joe-Edwards-GDS
body: Yeah, I think it probably will be from a CRI state. It might not be from non-CRI states (e.g. if we want to divert the user further down the line) - but it might still be obvious.

Happy to leave it and we can always change it later as we understand the impact more, up to you :)
created_at: 2025-03-11T15:35:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,6 +231,35 @@ const renderTransitions = (journeyStates, formData) => {
   return { transitionsMermaid: stateTransitions.join("\n"), states };
 };
 
+const createTransitionLabel = ({
+  eventName,
+  targetEntryEvent,
+  journeyContext,
+  mitigation,
+}) => {
+  const eventLabel = `${eventName}${targetEntryEvent ? `/${targetEntryEvent}` : ""}`;
+  const journeyContextLabel = journeyContext
+    ? ` - journeyContext: ${journeyContext}`
+    : "";
+  const mitigationLabel = mitigation ? `CI mitigation: ${mitigation}` : "";
+
+  const labelWithClass = (className, eventTransitionLabel) =>
+    `<span class="${className}">${eventTransitionLabel}</span>`;
+
+  if (journeyContext) {
+    return labelWithClass(
+      JOURNEY_CONTEXT_TRANSITION_CLASSNAME,
+      eventLabel + journeyContextLabel,
+    );
+  }
+
+  if (mitigationLabel) {
+    return labelWithClass(MITIGATIONS_TRANSITION_CLASSNAME, mitigationLabel);
path: journey-map/public/render.mjs
user: login: thebauSoftwire
body: that's a fair point, will add back in :)
created_at: 2025-03-11T16:07:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -32,8 +38,13 @@ public class BasicEvent implements Event {
     private LinkedHashMap<String, Event> checkJourneyContext;
     private List<AuditEventTypes> auditEvents;
     private LinkedHashMap<String, String> auditContext;
+    private LinkedHashMap<String, Event> checkMitigation;
path: lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/BasicEvent.java
user: login: thebauSoftwire
body: ahh missed this, thanks!
created_at: 2025-03-11T16:23:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -73,12 +84,53 @@ public TransitionResult resolve(JourneyContext journeyContext) throws UnknownEve
             if (firstFeatureFlag.isPresent()) {
                 String featureFlagValue = firstFeatureFlag.get();
                 LOGGER.info("Feature flag '{}' is set. Using alternative event", featureFlagValue);
-                return checkFeatureFlag.get(featureFlagValue).resolve(journeyContext);
+                return checkFeatureFlag.get(featureFlagValue).resolve(resolveParameters);
             }
         }
+        if (checkMitigation != null) {
+            var matchedMitigationEvent = getMitigationEvent(resolveParameters);
+
+            if (matchedMitigationEvent.isPresent()) {
+                var mitigationEvent = matchedMitigationEvent.get();
+                LOGGER.info("Mitigation found. Using alternative event.");
+                return mitigationEvent.resolve(resolveParameters);
path: lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/BasicEvent.java
user: login: Joe-Edwards-GDS
body: Feels a bit weird to move the `containsKey` away from where the check is needed, but happy to work around.

(Also - if you ever think Sonar is directing you to make worse code, we can mark as a false positive/wontfix, it's only there to help us :) )
created_at: 2025-03-11T16:50:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -73,12 +84,53 @@ public TransitionResult resolve(JourneyContext journeyContext) throws UnknownEve
             if (firstFeatureFlag.isPresent()) {
                 String featureFlagValue = firstFeatureFlag.get();
                 LOGGER.info("Feature flag '{}' is set. Using alternative event", featureFlagValue);
-                return checkFeatureFlag.get(featureFlagValue).resolve(journeyContext);
+                return checkFeatureFlag.get(featureFlagValue).resolve(resolveParameters);
             }
         }
+        if (checkMitigation != null) {
+            var matchedMitigationEvent = getMitigationEvent(resolveParameters);
+
+            if (matchedMitigationEvent.isPresent()) {
+                var mitigationEvent = matchedMitigationEvent.get();
+                LOGGER.info("Mitigation found. Using alternative event.");
+                return mitigationEvent.resolve(resolveParameters);
path: lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/BasicEvent.java
user: login: thebauSoftwire
body: yeah I agree, it was more so to stop sonar from complaining :') but I don't think marking this as a false positive/won't fix would be good as it might be worth refactoring this method at some point when we start adding other checks which push it past the threshold
created_at: 2025-03-11T16:55:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -73,12 +84,53 @@ public TransitionResult resolve(JourneyContext journeyContext) throws UnknownEve
             if (firstFeatureFlag.isPresent()) {
                 String featureFlagValue = firstFeatureFlag.get();
                 LOGGER.info("Feature flag '{}' is set. Using alternative event", featureFlagValue);
-                return checkFeatureFlag.get(featureFlagValue).resolve(journeyContext);
+                return checkFeatureFlag.get(featureFlagValue).resolve(resolveParameters);
             }
         }
+        if (checkMitigation != null) {
+            var matchedMitigationEvent = getMitigationEvent(resolveParameters);
+
+            if (matchedMitigationEvent.isPresent()) {
+                var mitigationEvent = matchedMitigationEvent.get();
+                LOGGER.info("Mitigation found. Using alternative event.");
+                return mitigationEvent.resolve(resolveParameters);
path: lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/BasicEvent.java
user: login: Joe-Edwards-GDS
body: Sure - I'd love to do a complete rewrite of this stuff anyway (e.g. see my other comment), perhaps even into Typescript!

Not a priority yet, but could be later in the year...
created_at: 2025-03-11T17:03:37Z
author_association: CONTRIBUTOR
path: lambdas/process-journey-event/src/main/java/uk/gov/di/ipv/core/processjourneyevent/statemachine/events/EventResolveParameters.java
user: login: thebauSoftwire
body: I'm having a hard time picturing what this would look like, would this EventResolver class not act like a service essentially? So we'd pass in an event to it's `resolve` method but in order to initialise the service we'd either have to:
1. initialise EventResolver at the State level which means we'd need to either pass in a config service from higher up into the State classes or initialise a config service for every instance of the State class
2. initialise EventResolver at the ProcessJourneyEventHandler level which would mean passing it down to where the events are similarly to how we're passing down CimitUtilityService
created_at: 2025-03-11T17:25:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -106,6 +106,16 @@ private boolean isScoreBreachingCiThreshold(int score, Vot vot) {
                 > Integer.parseInt(configService.getParameter(CI_SCORING_THRESHOLD, vot.name()));
     }
 
+    public Optional<String> getMitigationJourneyEvent(
+            List<ContraIndicator> cis, Vot confidenceRequested) throws ConfigException {
+        var journeyResponse = getMitigationJourneyIfBreaching(cis, confidenceRequested);
path: libs/common-services/src/main/java/uk/gov/di/ipv/core/library/service/CimitUtilityService.java
user: login: Joe-Edwards-GDS
body: Sorry I missed this the first time - does this cover the second condition on the ticket:
> The user is not breaching the CI threshold and has a mitigated CI

It's a weird one, but I think we need it to (e.g.) ensure we still route the user on a mitigation route even if they've done a partial journey that mitigated the CI.
created_at: 2025-03-12T14:40:49Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-03-13T09:48:04Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=ipv-core-back&pullRequest=3031) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=ipv-core-back&pullRequest=3031&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=ipv-core-back&pullRequest=3031&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=ipv-core-back&pullRequest=3031&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [91.7% Coverage on New Code](https://sonarcloud.io/component_measures?id=ipv-core-back&pullRequest=3031&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=ipv-core-back&pullRequest=3031&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=ipv-core-back&pullRequest=3031)


author_association: NONE