package com.intrasoft.ermis.transit.management.core.service;

import static com.intrasoft.ermis.transit.common.enums.MessageDomainTypeEnum.COMMON;
import static com.intrasoft.ermis.transit.common.enums.OfficeRoleTypesEnum.DEPARTURE;
import static com.intrasoft.ermis.transit.common.enums.OfficeRoleTypesEnum.DESTINATION;
import static com.intrasoft.ermis.transit.common.enums.OfficeRoleTypesEnum.EXIT;
import static com.intrasoft.ermis.transit.common.enums.OfficeRoleTypesEnum.INCIDENT;
import static com.intrasoft.ermis.transit.common.enums.OfficeRoleTypesEnum.TRANSIT;
import static com.intrasoft.ermis.transit.common.mappers.MessageTypeMapper.NTA;
import static com.intrasoft.ermis.transit.management.domain.ValidationConstants.DEP_ENQUIRY_ACTIONS_LIST;
import static com.intrasoft.ermis.transit.management.domain.ValidationConstants.DEP_RECOVERY_ACTIONS_LIST;
import static com.intrasoft.ermis.transit.management.domain.ValidationConstants.NON_DEP_RECOVERY_ACTIONS_LIST;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.intrasoft.ermis.common.json.parsing.JsonConverter;
import com.intrasoft.ermis.common.json.parsing.JsonConverterFactory;
import com.intrasoft.ermis.common.logging.ErmisLogger;
import com.intrasoft.ermis.common.xml.enums.NamespacesEnum;
import com.intrasoft.ermis.common.xml.parsing.XmlConverter;
import com.intrasoft.ermis.ddntav5152.messages.CC015CType;
import com.intrasoft.ermis.ddntav5152.messages.CC140CType;
import com.intrasoft.ermis.ddntav5152.messages.CC141CType;
import com.intrasoft.ermis.ddntav5152.messages.CD001CType;
import com.intrasoft.ermis.ddntav5152.messages.CD027CType;
import com.intrasoft.ermis.ddntav5152.messages.CD142CType;
import com.intrasoft.ermis.ddntav5152.messages.CD143CType;
import com.intrasoft.ermis.ddntav5152.messages.CD144CType;
import com.intrasoft.ermis.ddntav5152.messages.CD145CType;
import com.intrasoft.ermis.ddntav5152.messages.CaptureResultsOfControlEvent;
import com.intrasoft.ermis.ddntav5152.messages.CustomsOfficeOfRequestType01;
import com.intrasoft.ermis.ddntav5152.messages.EnquiryInformationEvent;
import com.intrasoft.ermis.ddntav5152.messages.EnquiryInformationRequestEvent;
import com.intrasoft.ermis.ddntav5152.messages.GuaranteeType02;
import com.intrasoft.ermis.ddntav5152.messages.ManualTraderEnquiryResponseEvent;
import com.intrasoft.ermis.ddntav5152.messages.MessageTypes;
import com.intrasoft.ermis.ddntav5152.messages.OfficeEnquiryResponseEvent;
import com.intrasoft.ermis.ddntav5152.messages.OfficeOfExitForTransitEvent;
import com.intrasoft.ermis.ddntav5152.messages.OfficeOfTransitEvent;
import com.intrasoft.ermis.ddntav5152.messages.OfficeRoleType;
import com.intrasoft.ermis.ddntav5152.messages.PhaseIDtype;
import com.intrasoft.ermis.ddntav5152.messages.RecoveryRequestToOtherCountryEvent;
import com.intrasoft.ermis.ddntav5152.messages.RequestOfficeEnquiryEvent;
import com.intrasoft.ermis.ddntav5152.messages.RequestTraderEnquiryEvent;
import com.intrasoft.ermis.ddntav5152.messages.TransitOperationType43;
import com.intrasoft.ermis.platform.shared.client.reference.data.core.ReferenceDataAdapter;
import com.intrasoft.ermis.platform.shared.client.reference.data.core.domain.CodeList;
import com.intrasoft.ermis.platform.shared.client.reference.data.core.domain.CodeListDomain;
import com.intrasoft.ermis.platform.shared.client.reference.data.core.domain.CodeListItem;
import com.intrasoft.ermis.transit.common.enums.ActionTypeEnum;
import com.intrasoft.ermis.transit.common.enums.DirectionEnum;
import com.intrasoft.ermis.transit.common.enums.EnquiryResponseCodeEnum;
import com.intrasoft.ermis.transit.common.enums.MessageDomainTypeEnum;
import com.intrasoft.ermis.transit.common.enums.MessageStatusEnum;
import com.intrasoft.ermis.transit.common.enums.OfficeOfDepartureWorkTaskTypeEnum;
import com.intrasoft.ermis.transit.common.enums.OfficeOfDestinationWorkTaskTypeEnum;
import com.intrasoft.ermis.transit.common.enums.OfficeOfExitForTransitWorkTaskTypeEnum;
import com.intrasoft.ermis.transit.common.enums.OfficeOfIncidentWorkTaskTypeEnum;
import com.intrasoft.ermis.transit.common.enums.OfficeOfLodgementWorkTaskTypeEnum;
import com.intrasoft.ermis.transit.common.enums.OfficeOfTransitWorkTaskTypeEnum;
import com.intrasoft.ermis.transit.common.enums.OfficeRoleTypeStatuses;
import com.intrasoft.ermis.transit.common.enums.OfficeRoleTypesEnum;
import com.intrasoft.ermis.transit.common.enums.ProcessingStatusTypeEnum;
import com.intrasoft.ermis.transit.common.enums.WorkTaskStatusEnum;
import com.intrasoft.ermis.transit.common.exceptions.BaseException;
import com.intrasoft.ermis.transit.common.exceptions.NotFoundException;
import com.intrasoft.ermis.transit.common.service.MessageHelperService;
import com.intrasoft.ermis.transit.common.util.MessageUtil;
import com.intrasoft.ermis.transit.contracts.core.enums.MessageXPathsEnum;
import com.intrasoft.ermis.transit.contracts.management.dto.action.data.TerminateTIRMovementEvent;
import com.intrasoft.ermis.transit.contracts.validation.enums.ErrorCode;
import com.intrasoft.ermis.transit.contracts.worktaskmanager.dto.CompleteWorkTaskDTO;
import com.intrasoft.ermis.transit.contracts.worktaskmanager.dto.WorkTaskPropertyDTO;
import com.intrasoft.ermis.transit.contracts.worktaskmanager.enums.WorkTaskPropertyKeysEnum;
import com.intrasoft.ermis.transit.management.core.action.ATRRequestAction;
import com.intrasoft.ermis.transit.management.core.action.AXRRequestAction;
import com.intrasoft.ermis.transit.management.core.action.AcceptGuaranteeAction;
import com.intrasoft.ermis.transit.management.core.action.CDMessageManagementAction;
import com.intrasoft.ermis.transit.management.core.action.CancelEnquiryAction;
import com.intrasoft.ermis.transit.management.core.action.CaptureResultsOfControlAction;
import com.intrasoft.ermis.transit.management.core.action.CompleteRecoveryDepAction;
import com.intrasoft.ermis.transit.management.core.action.CompleteRecoveryDestAction;
import com.intrasoft.ermis.transit.management.core.action.CompleteRecoveryExitAction;
import com.intrasoft.ermis.transit.management.core.action.CompleteRecoveryIncidentAction;
import com.intrasoft.ermis.transit.management.core.action.CompleteRecoveryTrsAction;
import com.intrasoft.ermis.transit.management.core.action.EnquiryInformationAction;
import com.intrasoft.ermis.transit.management.core.action.EnquiryInformationRequestAction;
import com.intrasoft.ermis.transit.management.core.action.IncidentNotificationAction;
import com.intrasoft.ermis.transit.management.core.action.IncidentNotificationDepAction;
import com.intrasoft.ermis.transit.management.core.action.IncidentNotificationDestAction;
import com.intrasoft.ermis.transit.management.core.action.IncidentNotificationTrsAction;
import com.intrasoft.ermis.transit.management.core.action.IncidentNotificationTrxsAction;
import com.intrasoft.ermis.transit.management.core.action.ManualTraderEnquiryResponse;
import com.intrasoft.ermis.transit.management.core.action.MovementQueryGlobalAction;
import com.intrasoft.ermis.transit.management.core.action.MovementQueryIncidentAction;
import com.intrasoft.ermis.transit.management.core.action.OfficeEnquiryResponseAction;
import com.intrasoft.ermis.transit.management.core.action.PostponeEnquiryAction;
import com.intrasoft.ermis.transit.management.core.action.RecommendEnquiryAction;
import com.intrasoft.ermis.transit.management.core.action.RecommendRecoveryAction;
import com.intrasoft.ermis.transit.management.core.action.RecoveryRequestToOtherCountryAction;
import com.intrasoft.ermis.transit.management.core.action.RecoveryResponseDepAction;
import com.intrasoft.ermis.transit.management.core.action.RecoveryResponseDestAction;
import com.intrasoft.ermis.transit.management.core.action.RecoveryResponseExitAction;
import com.intrasoft.ermis.transit.management.core.action.RecoveryResponseIncidentAction;
import com.intrasoft.ermis.transit.management.core.action.RecoveryResponseTrsAction;
import com.intrasoft.ermis.transit.management.core.action.RegisterOfficeOfExitForTransitAction;
import com.intrasoft.ermis.transit.management.core.action.RegisterOfficeOfTransitAction;
import com.intrasoft.ermis.transit.management.core.action.RejectGuaranteeAction;
import com.intrasoft.ermis.transit.management.core.action.RequestOfficeEnquiryAction;
import com.intrasoft.ermis.transit.management.core.action.RequestTraderEnquiryAction;
import com.intrasoft.ermis.transit.management.core.action.SealsAcknowledgementAction;
import com.intrasoft.ermis.transit.management.core.action.TerminateTIRMovementAction;
import com.intrasoft.ermis.transit.management.core.action.UpdateSealsAction;
import com.intrasoft.ermis.transit.management.core.action.WorkTaskAction;
import com.intrasoft.ermis.transit.management.core.action.base.ActionFactory;
import com.intrasoft.ermis.transit.management.core.action.conditions.base.ActionFilterPredicateEvaluator;
import com.intrasoft.ermis.transit.management.core.action.metadata.base.ActionMetadataFetcher;
import com.intrasoft.ermis.transit.management.core.action.predicates.CodeListExistenceFilterPredicate;
import com.intrasoft.ermis.transit.management.core.action.predicates.EnquiryStatusFilterPredicate;
import com.intrasoft.ermis.transit.management.core.action.predicates.MovementDeclarationTypeFilterPredicate;
import com.intrasoft.ermis.transit.management.core.action.predicates.MovementMessageHistoryFilterPredicate;
import com.intrasoft.ermis.transit.management.core.action.predicates.MovementOpenWorkTasksFilterPredicate;
import com.intrasoft.ermis.transit.management.core.action.predicates.MovementSimplifiedFilterPredicate;
import com.intrasoft.ermis.transit.management.core.action.predicates.MovementStatusFilterPredicate;
import com.intrasoft.ermis.transit.management.core.action.predicates.OfficeRoleFilterPredicate;
import com.intrasoft.ermis.transit.management.core.action.predicates.UserRoleFilterPredicate;
import com.intrasoft.ermis.transit.management.core.action.predicates.base.ActionFilterCriteria;
import com.intrasoft.ermis.transit.management.core.exception.BadRequestException;
import com.intrasoft.ermis.transit.management.core.port.inbound.ActionService;
import com.intrasoft.ermis.transit.management.core.port.outbound.CcnMessageProducer;
import com.intrasoft.ermis.transit.management.core.port.outbound.DossierDataService;
import com.intrasoft.ermis.transit.management.core.port.outbound.ProcessMessageCommandProducer;
import com.intrasoft.ermis.transit.management.core.port.outbound.ValidationService;
import com.intrasoft.ermis.transit.management.core.port.outbound.WorkTaskManagerClient;
import com.intrasoft.ermis.transit.management.core.util.MessageValidationUtils;
import com.intrasoft.ermis.transit.management.core.util.ViolationUtils;
import com.intrasoft.ermis.transit.management.domain.Action;
import com.intrasoft.ermis.transit.management.domain.Declaration;
import com.intrasoft.ermis.transit.management.domain.EnquiryAdditionalInformation;
import com.intrasoft.ermis.transit.management.domain.Message;
import com.intrasoft.ermis.transit.management.domain.MessageAttribute;
import com.intrasoft.ermis.transit.management.domain.Movement;
import com.intrasoft.ermis.transit.management.domain.MovementQuery;
import com.intrasoft.ermis.transit.management.domain.SimpleAction;
import com.intrasoft.ermis.transit.management.domain.ValidationConstants;
import com.intrasoft.ermis.transit.management.domain.Violation;
import com.intrasoft.ermis.transit.management.domain.WorkTask;
import com.intrasoft.ermis.transit.shared.security.service.UserDetailsService;
import com.intrasoft.proddev.referencedata.shared.enums.CodelistKeyEnum;
import java.io.StringReader;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.annotation.PostConstruct;
import javax.xml.bind.JAXBException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

@RequiredArgsConstructor
@Service
public class ActionServiceImpl implements ActionService {

    private static final String MOVEMENT_BELONGS_TO_WRONG_OFFICE_ROLE_TYPE = "Movement belongs to wrong office role type";

    private static final String UNHANDLED_SCENARIO = "Unhandled scenario";

    private static final String MOVEMENT_WRONG_STATUS = "Not supported in status %s";

    private static final String MOVEMENT_NOT_FOUND = "Movement was not found";

    private static final String ACTION_HANDLING_ERROR = "Could not handle %s. %s";

    private static final String PAYLOAD_EMPTY_ERROR = "Action payload is empty";

    private static final String MOVEMENT_ID_EMPTY_ERROR = "Movement ID is empty";

    private static final String MRN_MOVEMENT_ID_EMPTY_ERROR = "MRN/Movement ID are empty";

    private static final String MRN_EMPTY_ERROR = "MRN is empty";

    private static final String OFFICE_ROLE_EMPTY_ERROR = "Office role is empty";

    /**
     * Instead of hardcoded : "com.intrasoft.ermis.ddntav5152.messages." retrieve the package name from any message type Class
     */
    private static final String DDNTA_MESSAGES_PACKAGE = CD001CType.class.getPackageName() + ".";

    public static final String EMPTY_MESSAGE_MRN = "Empty message/mrn.";

    private static final String ACTION_MESSAGE = "actionMessage";

    private static final String CD027C_MESSAGE = "cD027CMessage";

    public static final String RELEASE_INDICATOR = "releaseIndicator";

    public static final String MESSAGE_GROSS_MASS_PATH = "/Consignment/HouseConsignment/grossMass";

    private static final String XSD_ERROR_REASON = "XSD";

    private static final String INVALID_GROSS_MASS_VALUE = "The value '0' of element 'grossMass' is not valid";

    private final ErmisLogger log;

    private final DossierDataService dossierDataService;

    private final WorkTaskManagerClient workTaskManagerClient;

    private final ProcessMessageCommandProducer processMessageCommandProducer;

    private final CcnMessageProducer ccnMessageProducer;

    private final MessageHelperService messageHelperService;

    private final ObjectMapper objectMapper;

    private final ValidationService validationService;

    private final ReferenceDataAdapter referenceDataAdapter;

    private final UserDetailsService userDetailsService;

    private final List<? extends ActionFilterPredicateEvaluator> actionEvaluators;

    private final List<? extends ActionMetadataFetcher> actionMetadataFetchers;

    private static final JsonConverter JSON_CONVERTER = JsonConverterFactory.getDefaultJsonConverter();

    @Getter
    private final EnumMap<ActionTypeEnum, ActionFilterPredicateEvaluator> actionEvaluatorMap = new EnumMap<>(ActionTypeEnum.class);

    @Getter
    private final EnumMap<ActionTypeEnum, ActionMetadataFetcher> actionMetadataFetcherMap = new EnumMap<>(ActionTypeEnum.class);

    private static final List<OfficeRoleTypeStatuses> COMPLETE_RECOVERY_OFFICE_ROLE_TYPE_STATUSES_LIST = List.of(
            OfficeRoleTypeStatuses.of(DEPARTURE,
                    List.of(ProcessingStatusTypeEnum.OODEP_UNDER_RECOVERY_PROCEDURE.getItemCode())
            ),
            OfficeRoleTypeStatuses.of(DESTINATION,
                    List.of(ProcessingStatusTypeEnum.OODEST_UNDER_RECOVERY_PROCEDURE.getItemCode())
            ),
            OfficeRoleTypeStatuses.of(EXIT,
                    List.of(ProcessingStatusTypeEnum.OOTRXS_UNDER_RECOVERY_PROCEDURE.getItemCode())
            ),
            OfficeRoleTypeStatuses.of(INCIDENT,
                    List.of(ProcessingStatusTypeEnum.OOINC_UNDER_RECOVERY_PROCEDURE.getItemCode())
            ),
            OfficeRoleTypeStatuses.of(TRANSIT,
                    List.of(ProcessingStatusTypeEnum.OOTRS_UNDER_RECOVERY_PROCEDURE.getItemCode()))
    );

    private static final List<OfficeRoleTypeStatuses> RECOVERY_REQUEST_TO_OTHER_COUNTRY_OFFICE_ROLE_TYPE_STATUSES_LIST = List.of(
            OfficeRoleTypeStatuses.of(DEPARTURE,
                    List.of(ProcessingStatusTypeEnum.OODEP_RECOVERY_RECOMMENDED.getItemCode())
            ),
            OfficeRoleTypeStatuses.of(DESTINATION,
                    List.of(
                            ProcessingStatusTypeEnum.OODEST_NONE.getItemCode(),
                            ProcessingStatusTypeEnum.OODEST_AAR_CREATED.getItemCode(),
                            ProcessingStatusTypeEnum.OODEST_AAR_REQUESTED.getItemCode(),
                            ProcessingStatusTypeEnum.OODEST_ARRIVAL_ACCEPTED.getItemCode(),
                            ProcessingStatusTypeEnum.OODEST_DIVERSION_REJECTED.getItemCode(),
                            ProcessingStatusTypeEnum.OODEST_UNLOADING.getItemCode(),
                            ProcessingStatusTypeEnum.OODEST_UNLOADING_REMARKS.getItemCode(),
                            ProcessingStatusTypeEnum.OODEST_UNDER_CONTROL.getItemCode(),
                            ProcessingStatusTypeEnum.OODEST_WAITING_FOR_DISCREPANCIES_RESOLUTION.getItemCode(),
                            ProcessingStatusTypeEnum.OODEST_UNDER_RECOVERY_DECISION.getItemCode()
                    )
            ),
            OfficeRoleTypeStatuses.of(TRANSIT,
                    List.of(
                            ProcessingStatusTypeEnum.OOTRS_ATR_REJECTED.getItemCode(),
                            ProcessingStatusTypeEnum.OOTRS_NCF_REGISTERED.getItemCode(),
                            ProcessingStatusTypeEnum.OOTRS_MOVEMENT_TURNED_BACK.getItemCode(),
                            ProcessingStatusTypeEnum.OOTRS_MOVEMENT_STOPPED.getItemCode(),
                            ProcessingStatusTypeEnum.OOTRS_ATR_REQUESTED.getItemCode(),
                            ProcessingStatusTypeEnum.OOTRS_ATR_CREATED.getItemCode(),
                            ProcessingStatusTypeEnum.OOTRS_ARRIVED.getItemCode()
                    )
            ),
            OfficeRoleTypeStatuses.of(EXIT,
                    List.of(
                            ProcessingStatusTypeEnum.OOTRXS_AXR_REJECTED.getItemCode(),
                            ProcessingStatusTypeEnum.OOTRXS_MOVEMENT_LEFT_EUROPEAN_CUSTOMS_SECURITY_AREA.getItemCode(),
                            ProcessingStatusTypeEnum.OOTRXS_MOVEMENT_STOPPED.getItemCode(),
                            ProcessingStatusTypeEnum.OOTRXS_AXR_REQUESTED.getItemCode(),
                            ProcessingStatusTypeEnum.OOTRXS_AXR_CREATED.getItemCode(),
                            ProcessingStatusTypeEnum.OOTRXS_ARRIVED.getItemCode()
                    )
            ),
            OfficeRoleTypeStatuses.of(INCIDENT,
                    List.of(
                            ProcessingStatusTypeEnum.OOINC_INCIDENT_REGISTERED.getItemCode()
                    )
            ));

    private static final List<OfficeRoleTypeStatuses> CAPTURE_RESULTS_OF_CONTROL_OFFICE_ROLE_TYLE_STATUSES_LIST = List.of(
            OfficeRoleTypeStatuses.of(DESTINATION,
                    List.of(ProcessingStatusTypeEnum.OODEST_UNDER_CONTROL.getItemCode())
            ));

    private static final List<OfficeRoleTypeStatuses> RECOVERY_RESPONSE_OFFICE_ROLE_TYPE_STATUSES_LIST = List.of(
            OfficeRoleTypeStatuses.of(DEPARTURE,
                    List.of(ProcessingStatusTypeEnum.OODEP_RECOVERY_RECOMMENDED.getItemCode())
            ),
            OfficeRoleTypeStatuses.of(DESTINATION,
                    List.of(ProcessingStatusTypeEnum.OODEST_RECOVERY_REQUESTED.getItemCode())
            ),
            OfficeRoleTypeStatuses.of(INCIDENT,
                    List.of(ProcessingStatusTypeEnum.OOINC_RECOVERY_REQUESTED.getItemCode())
            ),
            OfficeRoleTypeStatuses.of(TRANSIT,
                    List.of(ProcessingStatusTypeEnum.OOTRS_RECOVERY_REQUESTED.getItemCode())
            ),
            OfficeRoleTypeStatuses.of(EXIT,
                    List.of(ProcessingStatusTypeEnum.OOTRXS_RECOVERY_REQUESTED.getItemCode())
            )
    );

    private static final List<OfficeRoleTypeStatuses> NLSA_OFFICE_ROLE_TYPE_STATUSES_LIST = List.of(
            OfficeRoleTypeStatuses.of(EXIT, List.of(
                    ProcessingStatusTypeEnum.OOTRXS_AXR_CREATED.getItemCode(),
                    ProcessingStatusTypeEnum.OOTRXS_MOVEMENT_LEFT_EUROPEAN_CUSTOMS_SECURITY_AREA.getItemCode()
            ))
    );

    private static final List<OfficeRoleTypeStatuses> ACCEPT_GUARANTEE_USAGE_OFFICE_ROLE_TYLE_STATUSES_LIST = List.of(
            OfficeRoleTypeStatuses.of(DEPARTURE,
                    List.of(ProcessingStatusTypeEnum.OODEP_GUARANTEE_UNDER_REGISTRATION.getItemCode())
            ));

    private static final List<OfficeRoleTypeStatuses> REJECT_GUARANTEE_USAGE_OFFICE_ROLE_TYLE_STATUSES_LIST = List.of(
            OfficeRoleTypeStatuses.of(DEPARTURE,
                    List.of(ProcessingStatusTypeEnum.OODEP_GUARANTEE_UNDER_REGISTRATION.getItemCode())
            ));

    private static final List<OfficeRoleTypeStatuses> SEALS_ACKNOWLEDGEMENT_STATUSES_LIST = List.of(
            OfficeRoleTypeStatuses.of(DESTINATION, List.of(
                    ProcessingStatusTypeEnum.OODEST_AAR_CREATED.getItemCode(),
                    ProcessingStatusTypeEnum.OODEST_ARRIVAL_ACCEPTED.getItemCode()
            ))
    );

    private static final String GUARANTEE_TYPES_W_REQUIRED_REFERENCE_CL = "CL076";

    public static final String REFERENCE_NUMBER_COMPTETENT_AUTHORITY_OF_RECOVERY = "ReferenceNumberCompetentAuthorityOfRecovery";

    public static final String REFERENCE_NUMBER_COMPTETENT_AUTHORITY_OF_ENQUIRY = "ReferenceNumberCompetentAuthorityOfEnquiry";

    @PostConstruct
    private void init() {
        actionEvaluators.forEach(evaluator -> actionEvaluatorMap.put(evaluator.actionType(), evaluator));
        actionMetadataFetchers.forEach(fetcher -> actionMetadataFetcherMap.put(fetcher.actionType(), fetcher));
    }

    @Override
    public List<Action> getGlobalActions(OfficeRoleTypesEnum officeRole, List<String> userRoles, String officeId) {
        ActionFilterCriteria criteria = new ActionFilterCriteria();
        criteria.put(UserRoleFilterPredicate.PROP_KEY, userRoles);
        criteria.put(OfficeRoleFilterPredicate.PROP_KEY, officeRole);
        criteria.put(CodeListExistenceFilterPredicate.PROP_KEY, MessageUtil.extractOfficeCountryCode(officeId));

        return Arrays.stream(ActionTypeEnum.values())
                     .filter(allowedActionsEnum -> !allowedActionsEnum.isRelatedMovementRequired())
                     .filter(action -> evaluateActionAvailability(criteria, action, Collections.emptyList()))
                     .map(action -> SimpleAction.builder()
                                                .actionType(action)
                                                .workTask(null) // Work tasks need to be associated with a movement by definition
                                                .build())
                     .collect(Collectors.toList());
    }

    @Override
    public List<Action> getMovementActions(OfficeRoleTypesEnum officeRole, String movementId, List<String> userRoles, String workingOfficeId) {
        log.debug("officeRole {}, movementId {}, userRoles {}, workingOfficeId {}", officeRole, movementId, userRoles, workingOfficeId);
        if (movementId == null) {
            return new ArrayList<>();
        }

        ActionFilterCriteria criteria = new ActionFilterCriteria();
        criteria.put(UserRoleFilterPredicate.PROP_KEY, userRoles);

        // Get movement status
        Movement movement = dossierDataService.getMovementById(movementId, false);
        if (Objects.isNull(movement)) {
            // If the movement is not found return no movement actions instead of breaking
            log.error("Movement \"" + movementId + "\" was not found while fetching available actions");
            return new ArrayList<>();
        }
        log.debug("got movement {}", movement);

        criteria.put(MovementMessageHistoryFilterPredicate.PROP_KEY, movement.getId());
        criteria.put(OfficeRoleFilterPredicate.PROP_KEY, OfficeRoleTypesEnum.of(movement.getOfficeRoleType()));
        criteria.put(MovementStatusFilterPredicate.PROP_KEY, ProcessingStatusTypeEnum.findByItemCode(movement.getProcessingStatus()));
        criteria.put(EnquiryStatusFilterPredicate.PROP_KEY, movement.getMrn());
        criteria.put(MovementSimplifiedFilterPredicate.PROP_KEY, String.valueOf(movement.isSimplified()));
        criteria.put(MovementDeclarationTypeFilterPredicate.PROP_KEY, Objects.isNull(movement.getDeclarationType()) ? "" : movement.getDeclarationType());

        log.debug("create criteria {}", criteria);

        // Get open work tasks
        List<WorkTask> openWorkTasks = workTaskManagerClient.findOpenWorkTasks(movementId);
        criteria.put(MovementOpenWorkTasksFilterPredicate.PROP_KEY, openWorkTasks);

        List<? extends Action<?>> actions = Arrays.stream(ActionTypeEnum.values())
                                                  .filter(ActionTypeEnum::isRelatedMovementRequired)
                                                  .filter(actionType -> evaluateActionAvailability(criteria, actionType, openWorkTasks))
                                                  .map(actionType -> setUpAction(movement, openWorkTasks, actionType))
                                                  .filter(obj -> !Objects.isNull(obj))
                                                  .toList();
        log.debug("created list of actions: {}", actions);

        return filterActionsBasedOnOfficeId(actions, movement, workingOfficeId);
    }

    private List<Action> filterActionsBasedOnOfficeId(List<? extends Action<?>> actions, Movement movement, String workingOfficeId) {
        switch (OfficeRoleTypesEnum.of(movement.getOfficeRoleType())) {
            case DEPARTURE:
                return filterDepartureMovements(actions, movement.getOfficeId(), workingOfficeId);
            case DESTINATION:
                return filterDestinationMovements(actions, movement.getOfficeId(), workingOfficeId);
            case TRANSIT:
                return filterTransitMovements(actions, movement, workingOfficeId);
            case EXIT:
                return filterExitMovements(actions, movement, workingOfficeId);
            default:
                return new ArrayList<>(actions);
        }
    }

    @SuppressWarnings("findsecbugs:XPATH_INJECTION")
    private List<Action> filterTransitMovements(List<? extends Action<?>> actions, Movement movement, String workingOfficeId) {
        try (StringReader sr = new StringReader(dossierDataService.getDeclaration(movement.getId()).getPayload())) {
            InputSource xml = new InputSource(sr);
            XPath xPath = XPathFactory.newInstance().newXPath();
            Node root = (Node) xPath.evaluate("/", xml, XPathConstants.NODE);

            NodeList declaredOfficeOfTransitIdList =
                    (NodeList) xPath.evaluate(MessageXPathsEnum.TRANSIT_DECLARED_OFFICE_ID.getValue(), root, XPathConstants.NODESET);
            NodeList actualOfficeOfTransitIdList =
                    (NodeList) xPath.evaluate(MessageXPathsEnum.TRANSIT_ACTUAL_OFFICE_ID.getValue(), root, XPathConstants.NODESET);

            boolean doesWorkingOfficeBelongToRecoveryOffices =
                    referenceDataAdapter.verifyCodeListItem(CodelistKeyEnum.CUSTOMS_OFFICE_RECOVERY.getKey(), workingOfficeId, CodeListDomain.NCTSP5);

            return actions.stream()
                          .filter(action -> {
                              if (ActionTypeEnum.REGISTER_NOTIFICATION_CROSSING_FRONTIER.equals(action.getActionType())) {
                                  return nodeListContainsValue(declaredOfficeOfTransitIdList, workingOfficeId);
                              } else if (ActionTypeEnum.MANUAL_ATR.equals(action.getActionType())) {
                                  return nodeListContainsValue(declaredOfficeOfTransitIdList, workingOfficeId)
                                          || nodeListContainsValue(actualOfficeOfTransitIdList, workingOfficeId);
                              } else if (NON_DEP_RECOVERY_ACTIONS_LIST.contains(action.getActionType())) {
                                  return doesWorkingOfficeBelongToRecoveryOffices;
                              } else {
                                  return workingOfficeId.equals(movement.getOfficeId());
                              }
                          })
                          .collect(Collectors.toList());
        } catch (XPathExpressionException e) {
            log.error("filterTransitMovements error on xpaths", e);
        }
        return List.of();
    }

    @SuppressWarnings("findsecbugs:XPATH_INJECTION")
    private List<Action> filterExitMovements(List<? extends Action<?>> actions, Movement movement, String workingOfficeId) {
        try (StringReader sr = new StringReader(dossierDataService.getDeclaration(movement.getId()).getPayload())) {
            InputSource xml = new InputSource(sr);
            XPath xPath = XPathFactory.newInstance().newXPath();
            Node root = (Node) xPath.evaluate("/", xml, XPathConstants.NODE);

            NodeList declaredOfficeOfExitIdList =
                    (NodeList) xPath.evaluate(MessageXPathsEnum.EXIT_FOR_TRANSIT_DECLARED_OFFICE_ID.getValue(), root, XPathConstants.NODESET);
            NodeList actualOfficeOfExitIdList =
                    (NodeList) xPath.evaluate(MessageXPathsEnum.EXIT_FOR_TRANSIT_ACTUAL_OFFICE_ID.getValue(), root, XPathConstants.NODESET);

            boolean doesWorkingOfficeBelongToRecoveryOffices =
                    referenceDataAdapter.verifyCodeListItem(CodelistKeyEnum.CUSTOMS_OFFICE_RECOVERY.getKey(), workingOfficeId, CodeListDomain.NCTSP5);

            return actions.stream()
                          .filter(action -> {
                              if (ActionTypeEnum.REGISTER_NOTIFICATION_LEAVING_SECURITY_AREA.equals(action.getActionType())) {
                                  return nodeListContainsValue(declaredOfficeOfExitIdList, workingOfficeId);
                              } else if (ActionTypeEnum.MLSA.equals(action.getActionType())) {
                                  return nodeListContainsValue(declaredOfficeOfExitIdList, workingOfficeId)
                                          || nodeListContainsValue(actualOfficeOfExitIdList, workingOfficeId);
                              } else if (NON_DEP_RECOVERY_ACTIONS_LIST.contains(action.getActionType())) {
                                  return doesWorkingOfficeBelongToRecoveryOffices;
                              } else {
                                  return workingOfficeId.equals(movement.getOfficeId());
                              }
                          })
                          .collect(Collectors.toList());
        } catch (XPathExpressionException e) {
            log.error("filterTransitMovements error on xpaths", e);
        }
        return List.of();
    }

    private boolean nodeListContainsValue(NodeList officeOfTransitIdList, String workingOfficeId) {
        if (officeOfTransitIdList.getLength() > 0) {
            for (int i = 0; i < officeOfTransitIdList.getLength(); i++) {
                if (officeOfTransitIdList.item(i).getNodeValue().equals(workingOfficeId)) {
                    return true;
                }
            }
        }
        return false;
    }

    private List<Action> filterDepartureMovements(List<? extends Action<?>> actions, String departureOfficeId, String workingOfficeId) {
        String competentAuthorityOfRecoveryDepartureOffice =
                referenceDataAdapter.getCodeListItemAttributeValueAsString(CodelistKeyEnum.LIST_OF_CUSTOM_OFFICES.getKey(), departureOfficeId,
                        REFERENCE_NUMBER_COMPTETENT_AUTHORITY_OF_RECOVERY, LocalDate.now(), CodeListDomain.GLOBAL);

        String competentAuthorityOfEnquiryDepartureOffice =
                referenceDataAdapter.getCodeListItemAttributeValueAsString(CodelistKeyEnum.LIST_OF_CUSTOM_OFFICES.getKey(), departureOfficeId,
                        REFERENCE_NUMBER_COMPTETENT_AUTHORITY_OF_ENQUIRY, LocalDate.now(), CodeListDomain.GLOBAL);

        return actions.stream()
                      .filter(action -> {
                          if (DEP_ENQUIRY_ACTIONS_LIST.contains(action.getActionType())) {
                              return competentAuthorityOfEnquiryDepartureOffice.equals(workingOfficeId);
                          } else if (DEP_RECOVERY_ACTIONS_LIST.contains(action.getActionType())) {
                              return competentAuthorityOfRecoveryDepartureOffice.equals(workingOfficeId);
                          } else if (ActionTypeEnum.STATUS_REQUEST.equals(action.getActionType())) {
                              return competentAuthorityOfEnquiryDepartureOffice.equals(workingOfficeId)
                                      || departureOfficeId.equals(workingOfficeId);
                          } else {
                              return departureOfficeId.equals(workingOfficeId);
                          }
                      })
                      .collect(Collectors.toList());
    }

    private List<Action> filterDestinationMovements(List<? extends Action<?>> actions, String destinationOfficeId, String workingOfficeId) {
        boolean doesWorkingOfficeBelongToRecoveryOffices =
                referenceDataAdapter.verifyCodeListItem(CodelistKeyEnum.CUSTOMS_OFFICE_RECOVERY.getKey(), workingOfficeId, CodeListDomain.NCTSP5);

        final String competentAuthorityOfEnquiryDestinationOffice =
                referenceDataAdapter.getCodeListItemAttributeValueAsString(CodelistKeyEnum.LIST_OF_CUSTOM_OFFICES.getKey(), destinationOfficeId,
                        REFERENCE_NUMBER_COMPTETENT_AUTHORITY_OF_ENQUIRY, LocalDate.now(), CodeListDomain.GLOBAL);

        return actions.stream()
                      .filter(action -> {
                          if (NON_DEP_RECOVERY_ACTIONS_LIST.contains(action.getActionType())) {
                              return doesWorkingOfficeBelongToRecoveryOffices;
                          } else if (ActionTypeEnum.REQUEST_ADDITIONAL_INFORMATION.equals(action.getActionType())) {
                              return competentAuthorityOfEnquiryDestinationOffice.equals(workingOfficeId);
                          } else {
                              return destinationOfficeId.equals(workingOfficeId);
                          }
                      })
                      .collect(Collectors.toList());
    }

    @Override
    @SuppressWarnings("unchecked")
    public void completeAction(Action action)
            throws BadRequestException, BaseException, UnsupportedOperationException, IllegalArgumentException {
        try {
            // If the action is connected to a work task we assume that the action's completion data are of the CompleteWorkTaskDTO type...
            if (action.getWorkTask() != null && action.getWorkTask().getWorkTaskId() != null) {
                Message message = dossierDataService.persistMessage(setUpActionMessage((WorkTaskAction) action));
                WorkTask workTask = workTaskManagerClient.getWorkTask(action.getWorkTask().getWorkTaskId());
                // TODO: Validate payload
                dossierDataService.associateMessageWithMovement(message.getId(), workTask.getMessageId());
                workTaskManagerClient.completeWorkTask(getCompleteWorkTaskDTOFromAction(action));
            } else {
                // ...else, handle the action as you want
                switch (action.getActionType()) {
                    case ATR_REQUEST_UNKNOWN_MRN:
                        handleATRRequestUnknownMRN((ATRRequestAction) action);
                        break;
                    case REGISTER_NOTIFICATION_CROSSING_FRONTIER:
                    case MANUAL_ATR:
                        handleOfficeOfTransit((RegisterOfficeOfTransitAction) action);
                        break;
                    case AXR_REQUEST_UNKNOWN_MRN:
                        handleAXRRequestUnknownMRN((AXRRequestAction) action);
                        break;
                    case REGISTER_NOTIFICATION_LEAVING_SECURITY_AREA:
                    case MLSA:
                        handleOfficeOfExitForTransitEvent((RegisterOfficeOfExitForTransitAction) action);
                        break;
                    case INCIDENT_NOTIFICATION:
                        handleIncidentNotification((IncidentNotificationAction) action);
                        break;
                    case INCIDENT_NOTIFICATION_DEP:
                        handleIncidentNotification((IncidentNotificationDepAction) action);
                        break;
                    case INCIDENT_NOTIFICATION_DEST:
                        handleIncidentNotification((IncidentNotificationDestAction) action);
                        break;
                    case INCIDENT_NOTIFICATION_TRXS:
                        handleIncidentNotification((IncidentNotificationTrxsAction) action);
                        break;
                    case INCIDENT_NOTIFICATION_TRS:
                        handleIncidentNotification((IncidentNotificationTrsAction) action);
                        break;
                    case MOVEMENT_QUERY_INCIDENT:
                        handleMovementQueryIncident((MovementQueryIncidentAction) action);
                        break;
                    case RECOMMEND_RECOVERY:
                        handleRecommendRecovery((RecommendRecoveryAction) action);
                        break;
                    case COMPLETE_RECOVERY_DEP:
                        handleCompleteRecovery((CompleteRecoveryDepAction) action);
                        break;
                    case COMPLETE_RECOVERY_DEST:
                        handleCompleteRecovery((CompleteRecoveryDestAction) action);
                        break;
                    case COMPLETE_RECOVERY_EXIT:
                        handleCompleteRecovery((CompleteRecoveryExitAction) action);
                        break;
                    case RECOVERY_REQUEST_TO_OTHER_COUNTRY:
                        handleRecoveryRequestToOtherCountry((RecoveryRequestToOtherCountryAction) action);
                        break;
                    case MOVEMENT_QUERY_GLOBAL:
                        handleMovementQueryGlobal((MovementQueryGlobalAction) action);
                        break;
                    case ENQUIRY_INFORMATION:
                        handleEnquiryInformation((EnquiryInformationAction) action);
                        break;
                    case RECOMMEND_ENQUIRY:
                        handleRecommendEnquiry((RecommendEnquiryAction) action);
                        break;
                    case POSTPONE_ENQUIRY:
                        handlePostponeEnquiry((PostponeEnquiryAction) action);
                        break;
                    case REQUEST_TRADER_ENQUIRY:
                        handleTraderEnquiry((RequestTraderEnquiryAction) action);
                        break;
                    case MANUAL_TRADER_ENQUIRY_RESPONSE:
                        handleManualTraderEnquiryResponse((ManualTraderEnquiryResponse) action);
                        break;
                    case REQUEST_OFFICE_ENQUIRY:
                        handleOfficeEnquiry((RequestOfficeEnquiryAction) action);
                        break;
                    case CANCEL_ENQUIRY_PROCEDURE:
                        handleCancelEnquiry((CancelEnquiryAction) action);
                        break;
                    case OFFICE_ENQUIRY_RESPONSE:
                        handleOfficeEnquiryResponse((OfficeEnquiryResponseAction) action);
                        break;
                    case ENQUIRY_INFORMATION_REQUEST:
                        handleEnquiryInformationRequest((EnquiryInformationRequestAction) action);
                        break;
                    case CAPTURE_RESULTS_OF_CONTROL:
                        handleCaptureResultsOfControl((CaptureResultsOfControlAction) action);
                        break;
                    case RECOVERY_RESPONSE_DEP:
                        handleRecoveryResponse((RecoveryResponseDepAction) action);
                        break;
                    case RECOVERY_RESPONSE_DEST:
                        handleRecoveryResponse((RecoveryResponseDestAction) action);
                        break;
                    case RECOVERY_RESPONSE_EXIT:
                        handleRecoveryResponse((RecoveryResponseExitAction) action);
                        break;
                    case RECOVERY_RESPONSE_TRS:
                        handleRecoveryResponse((RecoveryResponseTrsAction) action);
                        break;
                    case ACCEPT_GUARANTEE:
                        handleAcceptGuarantee((AcceptGuaranteeAction) action);
                        break;
                    case RECOVERY_RESPONSE_INCIDENT:
                        handleRecoveryResponse((RecoveryResponseIncidentAction) action);
                        break;
                    case COMPLETE_RECOVERY_INCIDENT:
                        handleCompleteRecovery((CompleteRecoveryIncidentAction) action);
                        break;
                    case COMPLETE_RECOVERY_TRS:
                        handleCompleteRecovery((CompleteRecoveryTrsAction) action);
                        break;
                    case CD_MESSAGE_MANAGEMENT:
                        handleCDMessageManagement((CDMessageManagementAction) action);
                        break;
                    case REJECT_GUARANTEE:
                        handleRejectGuarantee((RejectGuaranteeAction) action);
                        break;
                    case SEALS_ACKNOWLEDGEMENT:
                        handleSealsAcknowledgement((SealsAcknowledgementAction) action);
                        break;
                    case UPDATE_SEALS:
                        updateSeals((UpdateSealsAction) action);
                        break;
                    case TERMINATE_TIR_MOVEMENT:
                        handleTerminateTIRMovement((TerminateTIRMovementAction) action);
                        break;
                    default:
                        log.warn("Unhandled action type " + action.getActionType().name());
                }
            }
        } catch (BadRequestException | IllegalArgumentException e) {
            // Rethrow BadRequestExceptions and IllegalArgumentExceptions
            throw e;
        } catch (Exception e) {
            throw new BaseException(String.format(ACTION_HANDLING_ERROR, action.title(), e.getMessage()), e);
        }
    }

    private Action<?> setUpAction(Movement movement, List<WorkTask> openWorkTasks, ActionTypeEnum actionType) {
        boolean isWorkTaskRequired = actionType.getWorkTaskType() != null;

        if (isWorkTaskRequired) {
            Optional<WorkTask> workTask = openWorkTasks.stream()
                                                       .filter(getWorkTaskPredicate(OfficeRoleTypesEnum.of(movement.getOfficeRoleType()), actionType))
                                                       .findAny();

            // If the work task does not exist hide the action
            return workTask.map(task -> WorkTaskAction.builder()
                                                      .actionType(actionType)
                                                      .workTask(task)
                                                      .workTaskList(openWorkTasks)
                                                      .metadata(Map.of("worktaskProperties", Optional.ofNullable(task.getWorkTaskProperties())))
                                                      .build()).orElse(null);
        } else {
            Action<?> action = ActionFactory.getAction(actionType);
            return action.toBuilder()
                         .metadata(getActionMetadata(actionType, movement.getId()))
                         .build();
        }
    }

    private Map<String, Object> getActionMetadata(ActionTypeEnum actionType, String movementId) {
        return actionMetadataFetcherMap.containsKey(actionType)
                ? actionMetadataFetcherMap.get(actionType).fetchMetadata(movementId)
                : null;
    }

    private boolean evaluateActionAvailability(ActionFilterCriteria criteria, ActionTypeEnum action, List<WorkTask> openWorkTasks) {
        if (!actionEvaluatorMap.containsKey(action)) {
            // If no evaluator exists, then the action is always available
            return true;
        }

        ActionFilterPredicateEvaluator evaluator = actionEvaluatorMap.get(action);
        evaluator.getPredicates().forEach(predicate -> {
            predicate.setDossierDataService(dossierDataService);
            predicate.setReferenceDataAdapter(referenceDataAdapter);
            predicate.setMaxHistoricMessages(999);
            predicate.setOpenWorkTasks(openWorkTasks);
        });
        return evaluator.test(criteria);
    }

    private void handleATRRequestUnknownMRN(ATRRequestAction action) throws JsonProcessingException, BadRequestException {
        if (Objects.isNull(action.getPayload())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), PAYLOAD_EMPTY_ERROR));
        }

        Message message = dossierDataService.persistMessage(setUpActionMessage(action));
        validateMovementMessage(message);

        Movement movement;
        if (StringUtils.isBlank(action.getPayload().getMovementId())) {
            movement = dossierDataService.createMovement(setUpMovement(
                    action.getPayload().getMrn(),
                    OfficeRoleTypesEnum.TRANSIT.getValue(),
                    action.getPayload().getCustomsOfficeOfTransitActual().getReferenceNumber()
            ));
        } else {
            movement = getMovementById(action.getPayload().getMovementId());
            if (!ProcessingStatusTypeEnum.OOTRS_ATR_CREATED.getItemCode().equals(movement.getProcessingStatus()) &&
                    !ProcessingStatusTypeEnum.OOTRS_ATR_REJECTED.getItemCode().equals(movement.getProcessingStatus())) {
                throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(),
                        String.format(MOVEMENT_WRONG_STATUS, movement.getProcessingStatus())));
            }
        }

        dossierDataService.associateMessageWithMovement(message.getId(), movement.getId());
        processMessageCommandProducer.publish(message.getId(), message.getMessageType(), movement);
    }

    private void handleOfficeOfTransit(RegisterOfficeOfTransitAction action)
            throws JsonProcessingException, BadRequestException, BaseException {
        Message message;
        Movement movement;
        String createdMovementId = null;

        if (StringUtils.isBlank(action.getPayload().getMovementId())) {
            message = dossierDataService.persistMessage(setUpActionMessage(action));
            validateMovementMessage(message);

            // If movementId was not found
            if (!StringUtils.isBlank(action.getPayload().getMrn()) && !StringUtils.isBlank(action.getPayload().getOfficeId())) {
                try {
                    // ...first check if movement exists for the current office (it shouldn't in this code path)
                    movement = dossierDataService.findSingleMovementByCriteria(
                            action.getPayload().getMrn(),
                            TRANSIT.getValue(),
                            action.getPayload().getOfficeId(),
                            null
                    );
                } catch (Exception e) {
                    // ...else create a new one
                    movement = dossierDataService.createMovement(setUpMovement(
                            action.getPayload().getMrn(),
                            TRANSIT.getValue(),
                            action.getPayload().getOfficeId()
                    ));
                    createdMovementId = movement.getId();
                }
            } else {
                throw new BaseException(String.format(ACTION_HANDLING_ERROR, action.title(), MRN_MOVEMENT_ID_EMPTY_ERROR));
            }
        } else {
            movement = dossierDataService.getMovementById(action.getPayload().getMovementId(), false);
            validateMovementStatus(movement, ValidationConstants.NCF_OFFICE_ROLE_TYPE_STATUSES_LIST);

            // Fill-in the MRN to make handling @ OoTRS easier
            action.getPayload().setMrn(movement.getMrn());
            message = dossierDataService.persistMessage(setUpActionMessage(action));
            validateMovementMessage(message);
        }

        // Check that other office movements have the correct status
        List<Movement> relatedOfficeMovements = dossierDataService.findByCriteria(movement.getMrn(), null, TRANSIT.getValue(), null, 999);
        String finalCreatedMovementId = createdMovementId;
        relatedOfficeMovements
                .stream()
                .filter(relMov -> !relMov.getId().equals(finalCreatedMovementId))
                .forEach(relMov -> validateMovementStatus(relMov, ValidationConstants.NCF_OFFICE_ROLE_TYPE_STATUSES_LIST));

        dossierDataService.associateMessageWithMovement(message.getId(), movement.getId());
        if (action.getActionType().equals(ActionTypeEnum.MANUAL_ATR)) {
            CompleteWorkTaskDTO workTaskData = createWorkTaskCompletionData(action.getPayload(), message.getId());
            workTaskManagerClient.completeWorkTask(workTaskData);
        } else if (action.getActionType().equals(ActionTypeEnum.REGISTER_NOTIFICATION_CROSSING_FRONTIER)) {
            processMessageCommandProducer.publish(message.getId(), message.getMessageType(), movement);
        } else {
            throw new UnsupportedOperationException(UNHANDLED_SCENARIO);
        }
    }

    private CompleteWorkTaskDTO createWorkTaskCompletionData(OfficeOfTransitEvent event, String messageId) {
        return CompleteWorkTaskDTO.builder()
                                  .status(WorkTaskStatusEnum.ACCEPTED.getStatus())
                                  .workTaskId(event.getWorkTaskId())
                                  .submitter(userDetailsService.getUsername())
                                  .workTaskProperties(List.of(
                                          workTaskPropertyDTO(event.getWorkTaskId(), WorkTaskPropertyKeysEnum.ATR_HANDLING_DECISION, event.getDecision()),
                                          workTaskPropertyDTO(event.getWorkTaskId(), WorkTaskPropertyKeysEnum.MESSAGE_ID, messageId)
                                  ))
                                  .build();
    }

    private WorkTaskPropertyDTO workTaskPropertyDTO(String workTaskId, WorkTaskPropertyKeysEnum key, String value) {
        return WorkTaskPropertyDTO.builder()
                                  .workTaskId(workTaskId)
                                  .key(key.getPropertyKey())
                                  .value(value)
                                  .build();
    }

    private void handleOfficeOfExitForTransitEvent(RegisterOfficeOfExitForTransitAction action) throws JsonProcessingException,
            BadRequestException, BaseException {
        Message message;
        Movement movement;
        String createdMovementId = null;

        if (StringUtils.isBlank(action.getPayload().getMovementId())) {
            message = dossierDataService.persistMessage(setUpActionMessage(action));
            validateMovementMessage(message);

            // If movementId was not found
            if (!StringUtils.isBlank(action.getPayload().getMrn()) && !StringUtils.isBlank(action.getPayload().getOfficeId())) {
                try {
                    // ...first check if movement exists for the current office (it shouldn't in this code path)
                    movement = dossierDataService.findSingleMovementByCriteria(
                            action.getPayload().getMrn(),
                            EXIT.getValue(),
                            action.getPayload().getOfficeId(), // TODO: Get current officeId
                            null
                    );
                } catch (Exception e) {
                    // ...else create a new one
                    movement = dossierDataService.createMovement(setUpMovement(
                            action.getPayload().getMrn(),
                            EXIT.getValue(),
                            action.getPayload().getOfficeId() // TODO: Get current officeId
                    ));
                    createdMovementId = movement.getId();
                }
            } else {
                throw new BaseException(String.format(ACTION_HANDLING_ERROR, action.title(), MRN_MOVEMENT_ID_EMPTY_ERROR));
            }
        } else {
            movement = dossierDataService.getMovementById(action.getPayload().getMovementId(), false);
            validateMovementStatus(movement, NLSA_OFFICE_ROLE_TYPE_STATUSES_LIST);

            // Fill-in the MRN to make handling @ OoTRXS easier
            action.getPayload().setMrn(movement.getMrn());
            message = dossierDataService.persistMessage(setUpActionMessage(action));
            validateMovementMessage(message);
        }

        // Check that other office movements have the correct status
        List<Movement> relatedOfficeMovements = dossierDataService.findByCriteria(movement.getMrn(), null, EXIT.getValue(), null, 999);
        String finalCreatedMovementId = createdMovementId;
        relatedOfficeMovements
                .stream()
                .filter(relMov -> !relMov.getId().equals(finalCreatedMovementId))
                .forEach(relMov -> validateMovementStatus(relMov, NLSA_OFFICE_ROLE_TYPE_STATUSES_LIST));

        dossierDataService.associateMessageWithMovement(message.getId(), movement.getId());
        if (action.getActionType().equals(ActionTypeEnum.MLSA)) {
            CompleteWorkTaskDTO workTaskData = createWorkTaskCompletionData(action.getPayload());
            workTaskManagerClient.completeWorkTask(workTaskData);
        } else if (action.getActionType().equals(ActionTypeEnum.REGISTER_NOTIFICATION_LEAVING_SECURITY_AREA)) {
            processMessageCommandProducer.publish(message.getId(), message.getMessageType(), movement);
        } else {
            throw new UnsupportedOperationException(UNHANDLED_SCENARIO);
        }
    }

    private CompleteWorkTaskDTO createWorkTaskCompletionData(OfficeOfExitForTransitEvent event) {
        return CompleteWorkTaskDTO.builder()
                                  .status(WorkTaskStatusEnum.ACCEPTED.getStatus())
                                  .workTaskId(event.getWorkTaskId())
                                  .submitter(userDetailsService.getUsername())
                                  .workTaskProperties(List.of(
                                          WorkTaskPropertyDTO.builder()
                                                             .workTaskId(event.getWorkTaskId())
                                                             .key(WorkTaskPropertyKeysEnum.IS_PASSAGE_VALIDATED.getPropertyKey())
                                                             .value(String.valueOf(event.isDecision()))
                                                             .build()
                                  ))
                                  .build();
    }

    private void handleAXRRequestUnknownMRN(AXRRequestAction action) throws JsonProcessingException, BadRequestException {
        if (Objects.isNull(action.getPayload())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), PAYLOAD_EMPTY_ERROR));
        }

        Message message = dossierDataService.persistMessage(setUpActionMessage(action));
        validateMovementMessage(message);

        Movement movement = dossierDataService.createMovement(setUpMovement(
                action.getPayload().getMrn(),
                OfficeRoleTypesEnum.EXIT.getValue(),
                action.getPayload().getCustomsOfficeOfExitForTransitActual().getReferenceNumber()
        ));

        dossierDataService.associateMessageWithMovement(message.getId(), movement.getId());
        processMessageCommandProducer.publish(message.getId(), message.getMessageType(), movement);
    }

    private void handleRecommendRecovery(RecommendRecoveryAction action) throws JsonProcessingException, BadRequestException, BaseException {
        if (Objects.isNull(action.getPayload())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), PAYLOAD_EMPTY_ERROR));
        }
        if (StringUtils.isBlank(action.getPayload().getMovementId())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), MOVEMENT_ID_EMPTY_ERROR));
        }

        Message message = dossierDataService.persistMessage(setUpActionMessage(action));
        validateMovementMessage(message);
        Movement movement = getMovementById(action.getPayload().getMovementId());

        dossierDataService.associateMessageWithMovement(message.getId(), movement.getId());
        processMessageCommandProducer.publish(message.getId(), message.getMessageType(), movement);
    }

    private void handleCompleteRecovery(CompleteRecoveryDepAction action) throws JsonProcessingException, BadRequestException {
        if (Objects.isNull(action.getPayload())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), PAYLOAD_EMPTY_ERROR));
        }
        if (StringUtils.isBlank(action.getPayload().getMovementId())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), MOVEMENT_ID_EMPTY_ERROR));
        }

        Movement movement = getMovementById(action.getPayload().getMovementId());
        validateMovementStatus(movement, COMPLETE_RECOVERY_OFFICE_ROLE_TYPE_STATUSES_LIST);

        Message message = dossierDataService.persistMessage(setUpActionMessage(action));
        validateMovementMessage(message);

        dossierDataService.associateMessageWithMovement(message.getId(), movement.getId());
        workTaskManagerClient.completeWorkTask(createWorkTaskCompletionData(action.getPayload().getWorkTaskId(), Map.of(
                WorkTaskPropertyKeysEnum.MESSAGE_ID.getPropertyKey(), message.getId()
        )));
    }

    private void handleCompleteRecovery(CompleteRecoveryDestAction action) throws JsonProcessingException, BadRequestException {
        if (Objects.isNull(action.getPayload())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), PAYLOAD_EMPTY_ERROR));
        }
        if (StringUtils.isBlank(action.getPayload().getMovementId())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), MOVEMENT_ID_EMPTY_ERROR));
        }

        Movement movement = getMovementById(action.getPayload().getMovementId());
        validateMovementStatus(movement, COMPLETE_RECOVERY_OFFICE_ROLE_TYPE_STATUSES_LIST);

        Message message = dossierDataService.persistMessage(setUpActionMessage(action));
        validateMovementMessage(message);

        dossierDataService.associateMessageWithMovement(message.getId(), movement.getId());
        workTaskManagerClient.completeWorkTask(createWorkTaskCompletionData(action.getPayload().getWorkTaskId(), Map.of(
                WorkTaskPropertyKeysEnum.MESSAGE_ID.getPropertyKey(), message.getId()
        )));
    }

    private void handleCompleteRecovery(CompleteRecoveryIncidentAction action) throws JsonProcessingException, BadRequestException {
        if (Objects.isNull(action.getPayload())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), PAYLOAD_EMPTY_ERROR));
        }
        if (StringUtils.isBlank(action.getPayload().getMovementId())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), MOVEMENT_ID_EMPTY_ERROR));
        }

        Movement movement = getMovementById(action.getPayload().getMovementId());
        validateMovementStatus(movement, COMPLETE_RECOVERY_OFFICE_ROLE_TYPE_STATUSES_LIST);

        Message message = dossierDataService.persistMessage(setUpActionMessage(action));
        validateMovementMessage(message);

        dossierDataService.associateMessageWithMovement(message.getId(), movement.getId());
        workTaskManagerClient.completeWorkTask(createWorkTaskCompletionData(action.getPayload().getWorkTaskId(), Map.of(
                WorkTaskPropertyKeysEnum.MESSAGE_ID.getPropertyKey(), message.getId()
        )));
    }

    private void handleCompleteRecovery(CompleteRecoveryTrsAction action) throws JsonProcessingException, BadRequestException {
        if (Objects.isNull(action.getPayload())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), PAYLOAD_EMPTY_ERROR));
        }
        if (StringUtils.isBlank(action.getPayload().getMovementId())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), MOVEMENT_ID_EMPTY_ERROR));
        }

        Movement movement = getMovementById(action.getPayload().getMovementId());
        validateMovementStatus(movement, COMPLETE_RECOVERY_OFFICE_ROLE_TYPE_STATUSES_LIST);

        Message message = dossierDataService.persistMessage(setUpActionMessage(action));
        validateMovementMessage(message);

        dossierDataService.associateMessageWithMovement(message.getId(), movement.getId());
        workTaskManagerClient.completeWorkTask(createWorkTaskCompletionData(action.getPayload().getWorkTaskId(), Map.of(
                WorkTaskPropertyKeysEnum.MESSAGE_ID.getPropertyKey(), message.getId()
        )));
    }

    private void handleCompleteRecovery(CompleteRecoveryExitAction action) throws JsonProcessingException, BadRequestException {
        if (Objects.isNull(action.getPayload())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), PAYLOAD_EMPTY_ERROR));
        }
        if (StringUtils.isBlank(action.getPayload().getMovementId())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), MOVEMENT_ID_EMPTY_ERROR));
        }

        Movement movement = getMovementById(action.getPayload().getMovementId());
        validateMovementStatus(movement, COMPLETE_RECOVERY_OFFICE_ROLE_TYPE_STATUSES_LIST);

        Message message = dossierDataService.persistMessage(setUpActionMessage(action));
        validateMovementMessage(message);

        dossierDataService.associateMessageWithMovement(message.getId(), movement.getId());
        workTaskManagerClient.completeWorkTask(createWorkTaskCompletionData(action.getPayload().getWorkTaskId(), Map.of(
                WorkTaskPropertyKeysEnum.MESSAGE_ID.getPropertyKey(), message.getId()
        )));
    }

    private CompleteWorkTaskDTO createWorkTaskCompletionData(String workTaskId, Map<String, String> properties) {
        return CompleteWorkTaskDTO.builder()
                                  .workTaskId(workTaskId)
                                  .status(WorkTaskStatusEnum.ACCEPTED.getStatus())
                                  .submitter(userDetailsService.getUsername())
                                  .workTaskProperties(properties.entrySet().stream()
                                                                .map(entry -> WorkTaskPropertyDTO.builder()
                                                                                                 .workTaskId(workTaskId)
                                                                                                 .key(entry.getKey())
                                                                                                 .value(entry.getValue())
                                                                                                 .build())
                                                                .collect(Collectors.toList()))
                                  .build();
    }

    private void handleIncidentNotification(IncidentNotificationDepAction action) throws JsonProcessingException, BadRequestException {
        if (Objects.isNull(action.getPayload())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), PAYLOAD_EMPTY_ERROR));
        }
        if (StringUtils.isBlank(action.getPayload().getMovementId())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), MOVEMENT_ID_EMPTY_ERROR));
        }

        Movement movement = getMovementById(action.getPayload().getMovementId());

        Message message = dossierDataService.persistMessage(setUpActionMessage(action));
        validateMovementMessage(message);

        dossierDataService.associateMessageWithMovement(message.getId(), movement.getId());
        processMessageCommandProducer.publish(message.getId(), message.getMessageType(), movement);
    }

    private void handleIncidentNotification(IncidentNotificationDestAction action) throws JsonProcessingException, BadRequestException {
        if (Objects.isNull(action.getPayload())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), PAYLOAD_EMPTY_ERROR));
        }
        if (StringUtils.isBlank(action.getPayload().getMovementId())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), MOVEMENT_ID_EMPTY_ERROR));
        }

        Movement movement = getMovementById(action.getPayload().getMovementId());

        Message message = dossierDataService.persistMessage(setUpActionMessage(action));
        validateMovementMessage(message);

        dossierDataService.associateMessageWithMovement(message.getId(), movement.getId());
        processMessageCommandProducer.publish(message.getId(), message.getMessageType(), movement);
    }

    private void handleIncidentNotification(IncidentNotificationTrsAction action) throws JsonProcessingException, BadRequestException {
        if (Objects.isNull(action.getPayload())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), PAYLOAD_EMPTY_ERROR));
        }
        if (StringUtils.isBlank(action.getPayload().getMovementId())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), MOVEMENT_ID_EMPTY_ERROR));
        }

        Movement movement = getMovementById(action.getPayload().getMovementId());

        Message message = dossierDataService.persistMessage(setUpActionMessage(action));
        validateMovementMessage(message);

        dossierDataService.associateMessageWithMovement(message.getId(), movement.getId());
        processMessageCommandProducer.publish(message.getId(), message.getMessageType(), movement);
    }

    // TODO: The IE180 generation logic should probably be deferred to OoTrxs core logic
    private void handleIncidentNotification(IncidentNotificationTrxsAction action) throws JsonProcessingException, BadRequestException {
        if (Objects.isNull(action.getPayload())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), PAYLOAD_EMPTY_ERROR));
        }
        if (StringUtils.isBlank(action.getPayload().getMovementId())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), MOVEMENT_ID_EMPTY_ERROR));
        }

        Movement movement = getMovementById(action.getPayload().getMovementId());

        Message message = dossierDataService.persistMessage(setUpActionMessage(action));
        validateMovementMessage(message);

        dossierDataService.associateMessageWithMovement(message.getId(), movement.getId());
        processMessageCommandProducer.publish(message.getId(), message.getMessageType(), movement);
    }

    private void handleIncidentNotification(IncidentNotificationAction action) throws JsonProcessingException, BadRequestException {
        if (Objects.isNull(action.getPayload())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), PAYLOAD_EMPTY_ERROR));
        }
        if (StringUtils.isBlank(action.getPayload().getMovementId())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), MOVEMENT_ID_EMPTY_ERROR));
        }

        Movement movement = getMovementById(action.getPayload().getMovementId());

        Message message = dossierDataService.persistMessage(setUpActionMessage(action));
        validateMovementMessage(message);

        dossierDataService.associateMessageWithMovement(message.getId(), movement.getId());
        processMessageCommandProducer.publish(message.getId(), message.getMessageType(), movement);
    }

    private void handleMovementQueryIncident(MovementQueryIncidentAction action) throws JsonProcessingException, BadRequestException {

        if (!checkMovementNotExists(action.getPayload().getMrn(), INCIDENT,
                action.getPayload().getCustomsOfficeOfRequest().getReferenceNumber(), null)) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), "Movement already exists in Office of Incident"));
        }

        Movement movement;
        try {
            movement = dossierDataService.createMovement(setUpMovement(
                    action.getPayload().getMrn(),
                    OfficeRoleTypesEnum.INCIDENT.getValue(),
                    action.getPayload().getCustomsOfficeOfRequest().getReferenceNumber()
            ));

        } catch (Exception e) {
            throw new IllegalArgumentException(e.getMessage() + "\nProvided: " + String.format("MRN is: %s, Office Role Type is: %s, Office Id is: %s",
                    action.getPayload().getMrn(),
                    OfficeRoleTypesEnum.INCIDENT.getValue(),
                    action.getPayload().getCustomsOfficeOfRequest().getReferenceNumber()), e);
        }


        Map<String, Message> messageMap = genericMovementQueryHandle(
                createCD027CType(action, () -> action.getPayload().getMrn(), () -> action.getPayload().getCustomsOfficeOfRequest()),
                false,
                action);

        dossierDataService.associateMessageWithMovement(messageMap.get(ACTION_MESSAGE).getId(), movement.getId());
        dossierDataService.associateMessageWithMovement(messageMap.get(CD027C_MESSAGE).getId(), movement.getId());
        processMessageCommandProducer.publish(messageMap.get(CD027C_MESSAGE).getId(), messageMap.get(CD027C_MESSAGE).getMessageType(), movement);
    }

    private void handleRecoveryRequestToOtherCountry(RecoveryRequestToOtherCountryAction action)
            throws BadRequestException, BaseException, IllegalArgumentException, JsonProcessingException {

        if (Objects.isNull(action.getPayload())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), PAYLOAD_EMPTY_ERROR));
        } else if (action.getPayload().getOfficeRole() == null) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), OFFICE_ROLE_EMPTY_ERROR));
        }

        RecoveryRequestToOtherCountryEvent recoveryRequestEvent = action.getPayload();
        OfficeRoleType officeRole = recoveryRequestEvent.getOfficeRole();
        Movement movement;
        Message message;
        switch (officeRole) {
            case DEPARTURE:
                if (StringUtils.isBlank(action.getPayload().getMovementId())) {
                    throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), MOVEMENT_ID_EMPTY_ERROR));
                }

                movement = getMovementById(action.getPayload().getMovementId());
                validateMovementStatus(movement, RECOVERY_REQUEST_TO_OTHER_COUNTRY_OFFICE_ROLE_TYPE_STATUSES_LIST);

                // Persist and validate message
                message = dossierDataService.persistMessage(setUpActionMessage(action));
                validateMovementMessage(message);

                dossierDataService.associateMessageWithMovement(message.getId(), movement.getId());
                workTaskManagerClient.completeWorkTask(createWorkTaskCompletionData(action.getPayload().getWorkTaskId(), Map.of(
                        WorkTaskPropertyKeysEnum.MESSAGE_ID.getPropertyKey(), message.getId()
                )));
                break;
            case DESTINATION:
                if (StringUtils.isBlank(action.getPayload().getMrn())) {
                    throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), MRN_EMPTY_ERROR));
                }

                // Check if there is existing movement for this MRN
                String mrn = action.getPayload().getTransitOperation().getMRN();
                movement = getMovementByMrn(mrn);

                // Persist and validate message
                message = dossierDataService.persistMessage(setUpActionMessage(action));
                validateMovementMessage(message);

                // If movement doesn't exist on OoDest, create it
                if (movement == null && recoveryRequestEvent.getCustomsOfficeOfRecoveryRequested() != null
                        && recoveryRequestEvent.getCustomsOfficeOfRecoveryRequested().getReferenceNumber() != null) {
                    String officeId = recoveryRequestEvent.getCustomsOfficeOfRecoveryRequested().getReferenceNumber();
                    movement = dossierDataService.createMovement(setUpMovement(mrn, DESTINATION.getValue(), officeId));
                } else if (movement == null) {
                    String officeId = "DK11111";
                    movement = dossierDataService.createMovement(setUpMovement(mrn, DESTINATION.getValue(), officeId));
                }

                validateMovementStatus(movement, RECOVERY_REQUEST_TO_OTHER_COUNTRY_OFFICE_ROLE_TYPE_STATUSES_LIST);

                dossierDataService.associateMessageWithMovement(message.getId(), movement.getId());
                processMessageCommandProducer.publish(message.getId(), message.getMessageType(), movement);
                break;
            case TRANSIT:
            case EXIT:
            case INCIDENT:
                if (StringUtils.isBlank(action.getPayload().getMovementId())) {
                    throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), MOVEMENT_ID_EMPTY_ERROR));
                }

                movement = getMovementById(action.getPayload().getMovementId());
                validateMovementStatus(movement, RECOVERY_REQUEST_TO_OTHER_COUNTRY_OFFICE_ROLE_TYPE_STATUSES_LIST);

                // Persist and validate message
                message = dossierDataService.persistMessage(setUpActionMessage(action));
                validateMovementMessage(message);

                dossierDataService.associateMessageWithMovement(message.getId(), movement.getId());
                processMessageCommandProducer.publish(message.getId(), message.getMessageType(), movement);
                break;
            default:
                throw new BaseException("Action not supported for office role " + action.getPayload().getOfficeRole());
        }

    }

    private void handleRecoveryResponse(RecoveryResponseDepAction action) throws JsonProcessingException, BadRequestException {
        if (Objects.isNull(action.getPayload())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), PAYLOAD_EMPTY_ERROR));
        }
        if (StringUtils.isBlank(action.getPayload().getMovementId())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), MOVEMENT_ID_EMPTY_ERROR));
        }

        Movement movement = getMovementById(action.getPayload().getMovementId());
        validateMovementStatus(movement, RECOVERY_RESPONSE_OFFICE_ROLE_TYPE_STATUSES_LIST);

        Message message = dossierDataService.persistMessage(setUpActionMessage(action));
        validateMovementMessage(message);

        dossierDataService.associateMessageWithMovement(message.getId(), movement.getId());
        workTaskManagerClient.completeWorkTask(createWorkTaskCompletionData(action.getPayload().getWorkTaskId(), Map.of(
                WorkTaskPropertyKeysEnum.MESSAGE_ID.getPropertyKey(), message.getId()
        )));
    }

    private void handleRecoveryResponse(RecoveryResponseDestAction action) throws JsonProcessingException, BadRequestException {
        if (Objects.isNull(action.getPayload())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), PAYLOAD_EMPTY_ERROR));
        }
        if (StringUtils.isBlank(action.getPayload().getMovementId())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), MOVEMENT_ID_EMPTY_ERROR));
        }

        Movement movement = getMovementById(action.getPayload().getMovementId());
        validateMovementStatus(movement, RECOVERY_RESPONSE_OFFICE_ROLE_TYPE_STATUSES_LIST);

        Message message = dossierDataService.persistMessage(setUpActionMessage(action));
        validateMovementMessage(message);

        dossierDataService.associateMessageWithMovement(message.getId(), movement.getId());
        workTaskManagerClient.completeWorkTask(createWorkTaskCompletionData(action.getPayload().getWorkTaskId(), Map.of(
                WorkTaskPropertyKeysEnum.MESSAGE_ID.getPropertyKey(), message.getId()
        )));
    }

    @SneakyThrows
    private void handleRecoveryResponse(RecoveryResponseExitAction action) {
        if (Objects.isNull(action.getPayload())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), PAYLOAD_EMPTY_ERROR));
        }
        if (StringUtils.isBlank(action.getPayload().getMovementId())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), MOVEMENT_ID_EMPTY_ERROR));
        }

        Movement movement = getMovementById(action.getPayload().getMovementId());
        validateMovementStatus(movement, RECOVERY_RESPONSE_OFFICE_ROLE_TYPE_STATUSES_LIST);

        Message message = dossierDataService.persistMessage(setUpActionMessage(action));
        validateMovementMessage(message);

        dossierDataService.associateMessageWithMovement(message.getId(), movement.getId());
        workTaskManagerClient.completeWorkTask(createWorkTaskCompletionData(action.getPayload().getWorkTaskId(), Map.of(
                WorkTaskPropertyKeysEnum.MESSAGE_ID.getPropertyKey(), message.getId()
        )));
    }

    private void handleRecoveryResponse(RecoveryResponseTrsAction action) throws JsonProcessingException, BadRequestException {
        if (Objects.isNull(action.getPayload())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), PAYLOAD_EMPTY_ERROR));
        }
        if (StringUtils.isBlank(action.getPayload().getMovementId())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), MOVEMENT_ID_EMPTY_ERROR));
        }

        Movement movement = getMovementById(action.getPayload().getMovementId());
        validateMovementStatus(movement, RECOVERY_RESPONSE_OFFICE_ROLE_TYPE_STATUSES_LIST);

        Message message = dossierDataService.persistMessage(setUpActionMessage(action));
        validateMovementMessage(message);

        dossierDataService.associateMessageWithMovement(message.getId(), movement.getId());
        workTaskManagerClient.completeWorkTask(createWorkTaskCompletionData(action.getPayload().getWorkTaskId(), Map.of(
                WorkTaskPropertyKeysEnum.MESSAGE_ID.getPropertyKey(), message.getId()
        )));
    }

    private void handleRecoveryResponse(RecoveryResponseIncidentAction action) throws JsonProcessingException, BadRequestException {
        if (Objects.isNull(action.getPayload())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), PAYLOAD_EMPTY_ERROR));
        }
        if (StringUtils.isBlank(action.getPayload().getMovementId())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), MOVEMENT_ID_EMPTY_ERROR));
        }

        Movement movement = getMovementById(action.getPayload().getMovementId());
        validateMovementStatus(movement, RECOVERY_RESPONSE_OFFICE_ROLE_TYPE_STATUSES_LIST);

        Message message = dossierDataService.persistMessage(setUpActionMessage(action));
        validateMovementMessage(message);

        dossierDataService.associateMessageWithMovement(message.getId(), movement.getId());
        workTaskManagerClient.completeWorkTask(createWorkTaskCompletionData(action.getPayload().getWorkTaskId(), Map.of(
                WorkTaskPropertyKeysEnum.MESSAGE_ID.getPropertyKey(), message.getId()
        )));
    }

    private Movement getMovementByMrn(String mrn) {
        Movement movement = null;
        try {
            movement = dossierDataService.findSingleMovementByCriteria(mrn, DESTINATION.getValue(), null, null);
        } catch (NotFoundException e) {
            // If the movement is not found, that's fine
        }
        return movement;
    }

    private void handleEnquiryInformation(EnquiryInformationAction action) throws JsonProcessingException, BadRequestException {
        log.debug("handleEnquiryInformation()...");
        EnquiryInformationEvent enquiryInformationEvent = action.getPayload();
        if (enquiryInformationEvent == null) {
            throw new BaseException(
                    String.format(ACTION_HANDLING_ERROR, action.title(), PAYLOAD_EMPTY_ERROR));
        }

        CD144CType cd144CType = new CD144CType();
        cd144CType.setTransitOperation(action.getPayload().getTransitOperation());
        cd144CType.setCustomsOfficeOfDeparture(action.getPayload().getCustomsOfficeOfDeparture());
        cd144CType.setCompetentCustomsOfficeAtDeparture(action.getPayload().getCompetentCustomsOfficeAtDeparture());
        cd144CType.setCompetentCustomsOfficeAtDestination(action.getPayload().getCompetentCustomsOfficeAtDestination());
        cd144CType.setEnquiry(action.getPayload().getEnquiry());
        cd144CType.getResponseInformation().addAll(action.getPayload().getResponseInformation());

        if (cd144CType.getTransitOperation() == null || cd144CType.getTransitOperation().getMRN() == null) {
            throw new BaseException(String.format(ACTION_HANDLING_ERROR, action.title(),
                    EMPTY_MESSAGE_MRN));
        }

        Movement movement = getMovementById(action.getPayload().getMovementId());

        if (!DEPARTURE.getValue().equals(movement.getOfficeRoleType())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), MOVEMENT_BELONGS_TO_WRONG_OFFICE_ROLE_TYPE));
        }

        cd144CType.setMessageSender(messageHelperService.generateMessageSender());
        cd144CType.setMessageRecipient(generateMessageRecipientFromOfficeId(cd144CType.getCompetentCustomsOfficeAtDestination().getReferenceNumber()));
        cd144CType.setMessageType(MessageTypes.CD_144_C);
        cd144CType.setMessageIdentification(MessageUtil.generateMessageIdentification());
        cd144CType.setPreparationDateAndTime(LocalDateTime.now());
        cd144CType.setPhaseID(PhaseIDtype.NCTS_5_1);

        Message message =
                setUpActionMessage(action, cd144CType.getMessageIdentification(), cd144CType.getMessageSender(), cd144CType.getMessageRecipient());
        message = dossierDataService.persistMessage(message);
        dossierDataService.associateMessageWithMovement(message.getId(), action.getPayload().getMovementId());

        validateMovementMessage(message);

        Message taxudMessage = Message.builder()
                                      .messageIdentification(cd144CType.getMessageIdentification())
                                      .messageType(MessageTypes.CD_144_C.value())
                                      .source(cd144CType.getMessageSender())
                                      .destination(cd144CType.getMessageRecipient())
                                      .payload(XmlConverter.marshal(
                                              cd144CType,
                                              NamespacesEnum.NTA.getValue(),
                                              MessageTypes.CD_144_C.value()))
                                      .domain(COMMON.name())
                                      .direction(DirectionEnum.SENT.getDirection())
                                      .build();
        taxudMessage = dossierDataService.persistMessage(taxudMessage);
        dossierDataService.associateMessageWithMovement(taxudMessage.getId(), action.getPayload().getMovementId());

        EnquiryAdditionalInformation additionalInformation = EnquiryAdditionalInformation.builder()
                                                                                         .mrn(cd144CType.getTransitOperation().getMRN())
                                                                                         .messageType(MessageTypes.CD_144_C.value())
                                                                                         .requester(cd144CType.getCompetentCustomsOfficeAtDeparture()
                                                                                                              .getReferenceNumber())
                                                                                         .respondent(cd144CType.getCompetentCustomsOfficeAtDestination()
                                                                                                               .getReferenceNumber())
                                                                                         .officeRoleType(DEPARTURE.getValue())
                                                                                         .payload(
                                                                                                 XmlConverter.marshal(cd144CType,
                                                                                                         NamespacesEnum.NTA.getValue(),
                                                                                                         MessageTypes.CD_144_C.value()))
                                                                                         .build();
        dossierDataService.saveEnquiryAdditionalInformation(additionalInformation);

        ccnMessageProducer.publish(enquiryInformationEvent.getMovementId() != null ? enquiryInformationEvent.getMovementId() : enquiryInformationEvent.getMrn(),
                additionalInformation.getPayload(), MessageTypes.CD_144_C);
        log.debug("...handleEnquiryInformation()");
    }

    private void updateSeals(UpdateSealsAction action) throws JAXBException, JsonProcessingException, BadRequestException {
        log.debug("updateSeals()...");

        Message actionMessage = setUpActionMessage(action);

        actionMessage = dossierDataService.persistMessage(actionMessage);

        validateMovementMessage(actionMessage);

        dossierDataService.associateMessageWithMovement(actionMessage.getId(), action.getPayload().getMovementId());

        Declaration declaration = dossierDataService.getDeclaration(action.getPayload().getMovementId());

        CC015CType cc015CDeclaration = XmlConverter.buildMessageFromXML(declaration.getPayload(), CC015CType.class);

        if (cc015CDeclaration != null) {
            updateSealsIdentifier(action, cc015CDeclaration);

            String updatedCC015cXml = XmlConverter.marshal(cc015CDeclaration, NamespacesEnum.NTA.getValue(), MessageTypes.CC_015_C.value());
            Message updatedCC015CMessage = Message.builder()
                                                  .messageIdentification(cc015CDeclaration.getMessageIdentification())
                                                  .messageType(cc015CDeclaration.getMessageType().value())
                                                  .source(cc015CDeclaration.getMessageSender())
                                                  .destination(cc015CDeclaration.getMessageRecipient())
                                                  .domain(COMMON.name())
                                                  .payload(updatedCC015cXml)
                                                  .build();

            Declaration updatedDeclaration = Declaration.builder()
                                                        .movementId(action.getPayload().getMovementId())
                                                        .payloadType(MessageTypes.CC_015_C.value())
                                                        .payload(updatedCC015CMessage.getPayload())
                                                        .version(declaration.getVersion() + 1)
                                                        .build();

            dossierDataService.createDeclaration(updatedDeclaration);
        } else {
            throw new BaseException("Couldn't get CC015C message");
        }
        log.debug("...updateSeals()");
    }

    private static void updateSealsIdentifier(UpdateSealsAction action, CC015CType cc015cTypeMessage) {

        cc015cTypeMessage.getConsignment().getTransportEquipment().clear();
        cc015cTypeMessage.getConsignment().getTransportEquipment().addAll(action.getPayload().getTransportEquipment());

        cc015cTypeMessage.getConsignment().getTransportEquipment().forEach(transportEquipmentType06 ->
                transportEquipmentType06.setNumberOfSeals(transportEquipmentType06.getSeal().size()));

    }


    private void handleMovementQueryGlobal(MovementQueryGlobalAction action) throws JsonProcessingException, BadRequestException {
        log.debug("handleMovementQueryGlobal()...");

        CD027CType cd027CType = createCD027CType(action, () -> action.getPayload().getMrn(), () -> action.getPayload().getCustomsOfficeOfRequest());

        Map<String, Message> messageMap = genericMovementQueryHandle(cd027CType, true, action);

        ccnMessageProducer.publish(
                action.getPayload().getMovementId() != null ? action.getPayload().getMovementId() : cd027CType.getTransitOperation().getMRN(),
                messageMap.get(CD027C_MESSAGE).getPayload(), MessageTypes.CD_027_C);
        log.debug("...handleMovementQueryGlobal()");
    }

    private void createMovementQuery(CD027CType cd027CType, String payload) {
        MovementQuery movementQuery = MovementQuery.builder()
                                                   .mrn(cd027CType.getTransitOperation().getMRN())
                                                   .messageId(cd027CType.getMessageIdentification())
                                                   .requester(cd027CType.getCustomsOfficeOfRequest().getReferenceNumber())
                                                   .requestPayload(payload)
                                                   .build();
        dossierDataService.saveMovementQuery(movementQuery);
    }

    private void handleRecommendEnquiry(RecommendEnquiryAction action) throws JsonProcessingException, BadRequestException {
        log.debug("handleRecommendEnquiry({})", action.getPayload().getMovementId());

        Message message = dossierDataService.persistMessage(setUpActionMessage(action));
        validateMovementMessage(message);

        dossierDataService.associateMessageWithMovement(message.getId(), action.getPayload().getMovementId());
        processMessageCommandProducer.publish(message.getId(), message.getMessageType(), action.getPayload().getMovementId(), DEPARTURE.getValue());
    }

    private void handlePostponeEnquiry(PostponeEnquiryAction action) throws JsonProcessingException {
        log.debug("handlePostponeEnquiry({})", action.getPayload().getMovementId());

        Message message = dossierDataService.persistMessage(setUpActionMessage(action));
        // TODO: Validate incoming action payload
        //        validateMovementMessage(message);

        dossierDataService.associateMessageWithMovement(message.getId(), action.getPayload().getMovementId());

        processMessageCommandProducer.publish(message.getId(), message.getMessageType(), action.getPayload().getMovementId(), DEPARTURE.getValue());
    }

    private void handleTraderEnquiry(RequestTraderEnquiryAction action) throws JsonProcessingException, BadRequestException {
        log.debug("handleTraderEnquiry({})", action.getPayload().getMovementId());
        RequestTraderEnquiryEvent requestTraderEnquiryEvent = action.getPayload();
        if (requestTraderEnquiryEvent == null) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), PAYLOAD_EMPTY_ERROR));
        }

        String movementId = action.getPayload().getMovementId();
        if (movementId != null) {
            Movement movement = getMovementById(movementId);

            if (!DEPARTURE.getValue().equals(movement.getOfficeRoleType())) {
                throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), MOVEMENT_BELONGS_TO_WRONG_OFFICE_ROLE_TYPE));
            }
        }

        CC140CType cc140CType = new CC140CType();
        cc140CType.setTransitOperation(requestTraderEnquiryEvent.getTransitOperation());
        cc140CType.setCustomsOfficeOfDeparture(requestTraderEnquiryEvent.getCustomsOfficeOfDeparture());
        cc140CType.setCustomsOfficeOfEnquiryAtDeparture(requestTraderEnquiryEvent.getCustomsOfficeOfEnquiryAtDeparture());
        cc140CType.setHolderOfTheTransitProcedure(requestTraderEnquiryEvent.getHolderOfTheTransitProcedure());

        if (cc140CType.getTransitOperation() == null || cc140CType.getTransitOperation().getMRN() == null) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), EMPTY_MESSAGE_MRN));
        }

        setDataToCC140CMessage(cc140CType);

        Message message = setUpActionMessage(action);
        message = dossierDataService.persistMessage(message);
        validateMovementMessage(message);

        dossierDataService.associateMessageWithMovement(message.getId(), action.getPayload().getMovementId());
        processMessageCommandProducer.publish(message.getId(), message.getMessageType(), action.getPayload().getMovementId(), DEPARTURE.getValue());
    }

    private void handleManualTraderEnquiryResponse(ManualTraderEnquiryResponse action) throws JsonProcessingException, BadRequestException {
        log.debug("handleTraderResponseEnquiry({})", action.getPayload().getMovementId());
        ManualTraderEnquiryResponseEvent manualTraderEnquiryResponseEvent = action.getPayload();
        if (manualTraderEnquiryResponseEvent == null) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), PAYLOAD_EMPTY_ERROR));
        }

        String movementId = action.getPayload().getMovementId();
        if (movementId != null) {
            Movement movement = getMovementById(movementId);

            if (!DEPARTURE.getValue().equals(movement.getOfficeRoleType())) {
                throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), MOVEMENT_BELONGS_TO_WRONG_OFFICE_ROLE_TYPE));
            }
        }

        CC141CType cc141CType = createCC141CMessage(manualTraderEnquiryResponseEvent);

        if (cc141CType.getTransitOperation() == null || cc141CType.getTransitOperation().getMRN() == null) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), EMPTY_MESSAGE_MRN));
        }

        setDataToCC141CMessage(cc141CType);

        Message message = setUpActionMessage(action);
        message = dossierDataService.persistMessage(message);
        validateMovementMessage(message);
        dossierDataService.associateMessageWithMovement(message.getId(), action.getPayload().getMovementId());

        processMessageCommandProducer.publish(message.getId(), message.getMessageType(), action.getPayload().getMovementId(), DEPARTURE.getValue());
    }

    @NotNull
    private CC141CType createCC141CMessage(ManualTraderEnquiryResponseEvent manualTraderEnquiryResponseEvent) {
        CC141CType cc141CType = new CC141CType();
        cc141CType.setTransitOperation(manualTraderEnquiryResponseEvent.getTransitOperation());
        cc141CType.setCustomsOfficeOfEnquiryAtDeparture(manualTraderEnquiryResponseEvent.getCustomsOfficeOfEnquiryAtDeparture());
        cc141CType.setCustomsOfficeOfDestinationActual(manualTraderEnquiryResponseEvent.getCustomsOfficeOfDestinationActual());
        cc141CType.setHolderOfTheTransitProcedure(manualTraderEnquiryResponseEvent.getHolderOfTheTransitProcedure());
        cc141CType.setMessageRecipient(
                generateMessageRecipientFromOfficeId(manualTraderEnquiryResponseEvent.getCustomsOfficeOfEnquiryAtDeparture().getReferenceNumber()));
        cc141CType.setEnquiry(manualTraderEnquiryResponseEvent.getEnquiry());
        cc141CType.setConsignment(manualTraderEnquiryResponseEvent.getConsignment());
        return cc141CType;
    }

    private void handleOfficeEnquiry(RequestOfficeEnquiryAction action)
            throws BadRequestException, IllegalArgumentException, BaseException, JsonProcessingException {


        log.debug("handleOfficeEnquiry() for Movement with ID: " + action.getPayload().getMovementId());

        RequestOfficeEnquiryEvent requestOfficeEnquiryEvent = action.getPayload();

        if (requestOfficeEnquiryEvent == null) {
            throw new BaseException(String.format(ACTION_HANDLING_ERROR, action.title(), PAYLOAD_EMPTY_ERROR));
        }

        String movementId = requestOfficeEnquiryEvent.getMovementId();

        if (movementId != null) {

            Movement movement = getMovementById(movementId);

            if (!DEPARTURE.getValue().equals(movement.getOfficeRoleType())) {
                throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), MOVEMENT_BELONGS_TO_WRONG_OFFICE_ROLE_TYPE));
            }
        }

        if (requestOfficeEnquiryEvent.getTransitOperation() == null || requestOfficeEnquiryEvent.getTransitOperation().getMRN() == null) {
            throw new BaseException(String.format(ACTION_HANDLING_ERROR, action.title(), EMPTY_MESSAGE_MRN));
        }

        CD142CType cd142CType = new CD142CType();
        cd142CType.setTransitOperation(requestOfficeEnquiryEvent.getTransitOperation());
        cd142CType.setCustomsOfficeOfDeparture(requestOfficeEnquiryEvent.getCustomsOfficeOfDeparture());
        cd142CType.setCustomsOfficeOfRequest(requestOfficeEnquiryEvent.getCustomsOfficeOfRequest());
        cd142CType.setCustomsOfficeOfEnquiryAtDeparture(requestOfficeEnquiryEvent.getCustomsOfficeOfEnquiryAtDeparture());
        cd142CType.setEnquiry(requestOfficeEnquiryEvent.getEnquiry());
        cd142CType.setConsignment(requestOfficeEnquiryEvent.getConsignment());

        setDataToCD142CMessage(cd142CType);

        Message message = dossierDataService.persistMessage(setUpActionMessage(action));
        validateMovementMessage(message);

        dossierDataService.associateMessageWithMovement(message.getId(), action.getPayload().getMovementId());
        processMessageCommandProducer.publish(message.getId(), message.getMessageType(), action.getPayload().getMovementId(), DEPARTURE.getValue());
    }

    private void handleCancelEnquiry(CancelEnquiryAction action) throws JsonProcessingException {
        log.debug("handleCancelEnquiry({})", action.getPayload().getMovementId());

        Message message = dossierDataService.persistMessage(setUpActionMessage(action));

        dossierDataService.associateMessageWithMovement(message.getId(), action.getPayload().getMovementId());
        processMessageCommandProducer.publish(message.getId(), message.getMessageType(), action.getPayload().getMovementId(), DEPARTURE.getValue());
    }

    private void handleOfficeEnquiryResponse(OfficeEnquiryResponseAction action) throws JsonProcessingException, BadRequestException {
        log.debug("handleOfficeEnquiryResponse({})", action.getPayload().getMovementId());
        OfficeEnquiryResponseEvent officeEnquiryResponseEvent = action.getPayload();
        if (officeEnquiryResponseEvent == null) {
            throw new BaseException(String.format(ACTION_HANDLING_ERROR, action.title(), PAYLOAD_EMPTY_ERROR));
        }

        String movementId = action.getPayload().getMovementId();
        Movement movement = null;
        if (movementId != null) {
            movement = getMovementById(movementId);

            if (!DESTINATION.getValue().equals(movement.getOfficeRoleType())) {
                throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), MOVEMENT_BELONGS_TO_WRONG_OFFICE_ROLE_TYPE));
            }
        } else if (Objects.nonNull(action.getPayload()) && Objects.nonNull(action.getPayload().getEnquiry()) &&
                EnquiryResponseCodeEnum.REQUEST_FOR_RECOVERY_AT_DEST.getCode().equals(action.getPayload().getEnquiry().getResponseCode())) {
            movement = dossierDataService.createMovement(setUpMovement(
                    action.getPayload().getTransitOperation().getMRN(),
                    DESTINATION.getValue(),
                    action.getPayload().getCustomsOfficeOfRequest().getReferenceNumber()
            ));
        }

        CD143CType cd143CType = new CD143CType();
        cd143CType.setTransitOperation(officeEnquiryResponseEvent.getTransitOperation());
        cd143CType.setCustomsOfficeOfRequest(officeEnquiryResponseEvent.getCustomsOfficeOfRequest());
        cd143CType.setCustomsOfficeOfEnquiryAtDeparture(officeEnquiryResponseEvent.getCustomsOfficeOfEnquiryAtDeparture());
        cd143CType.setEnquiry(officeEnquiryResponseEvent.getEnquiry());
        cd143CType.setMessageRecipient(generateMessageRecipientFromOfficeId(officeEnquiryResponseEvent.getCustomsOfficeOfRequest().getReferenceNumber()));

        if (cd143CType.getTransitOperation() == null || cd143CType.getTransitOperation().getMRN() == null) {
            throw new BaseException(String.format(ACTION_HANDLING_ERROR, action.title(), EMPTY_MESSAGE_MRN));
        }

        setDataToCD143CMessage(cd143CType);

        Message message = setUpActionMessage(action, cd143CType.getMessageIdentification(), cd143CType.getMessageSender(), cd143CType.getMessageRecipient());
        message = dossierDataService.persistMessage(message);
        validateMovementMessage(message);
        if (Objects.nonNull(movement)) {
            dossierDataService.associateMessageWithMovement(message.getId(), movement.getId());
        }

        processMessageCommandProducer.publish(message.getId(), message.getMessageType(), Objects.nonNull(movement) ? movement.getId() : null,
                DESTINATION.getValue());
    }

    private void handleCaptureResultsOfControl(CaptureResultsOfControlAction action) throws JsonProcessingException, BadRequestException {
        Movement movement = dossierDataService.getMovementById(action.getPayload().getMovementId(), false);
        validateMovementStatus(movement, CAPTURE_RESULTS_OF_CONTROL_OFFICE_ROLE_TYLE_STATUSES_LIST);

        Message message = dossierDataService.persistMessage(setUpActionMessage(action));
        validateGrossMass(message);
        validateMovementMessage(message);

        dossierDataService.associateMessageWithMovement(message.getId(), movement.getId());

        if(action.getMetadata() != null) {
            handleReleaseIndicator(action.getMetadata(), message.getId());
        }

        processMessageCommandProducer.publish(message.getId(), message.getMessageType(), action.getPayload().getMovementId(), DESTINATION.getValue());
    }

    private void handleReleaseIndicator(Map<String, Object> actionMetadata, String messageId) {
        String releaseIndicatorValue = (String) actionMetadata.get(RELEASE_INDICATOR);
        if (StringUtils.isNotEmpty(releaseIndicatorValue)) {
            log.info("[partial]handleReleaseIndicator() - Found releaseIndicator with value:", releaseIndicatorValue);
            MessageAttribute messageAttribute = MessageAttribute.builder()
                                                                .messageId(messageId)
                                                                .attrKey(RELEASE_INDICATOR)
                                                                .attrValue(releaseIndicatorValue)
                                                                .build();
            dossierDataService.saveMessageAttribute(messageAttribute);
        }
    }

    private void validateGrossMass(Message message) throws BadRequestException {
        List<Violation> violations = new ArrayList<>();
        CaptureResultsOfControlEvent captureResultsOfControlEvent =
                JSON_CONVERTER.convertStringToObject(message.getPayload(), CaptureResultsOfControlEvent.class);
        if (captureResultsOfControlEvent == null  || captureResultsOfControlEvent.getConsignment() == null ||
                captureResultsOfControlEvent.getConsignment().getHouseConsignment() == null) {
            return;
        }
        if (captureResultsOfControlEvent.getConsignment()
                                    .getHouseConsignment()
                                    .stream()
                                    .anyMatch(cd018HouseConsignment -> cd018HouseConsignment.getGrossMass() != null &&
                                            BigDecimal.ZERO.compareTo(cd018HouseConsignment.getGrossMass()) == 0)) {
            log.info("validate IE018() : Invalid value '0' in gross mass");
            violations.add(ViolationUtils.createViolation(message.getMessageType().concat(MESSAGE_GROSS_MASS_PATH),
                    INVALID_GROSS_MASS_VALUE,
                    XSD_ERROR_REASON,
                    ErrorCode.INVALID_XML_FORMAT.getCode()));
            throw new BadRequestException(violations);
        }
    }

    private void handleEnquiryInformationRequest(EnquiryInformationRequestAction action)
            throws BadRequestException, BaseException, IllegalArgumentException, JsonProcessingException {

        log.debug("handleEnquiryInformationRequest()...");

        EnquiryInformationRequestEvent enquiryInformationRequestEvent = action.getPayload();

        if (enquiryInformationRequestEvent == null) {
            throw new BaseException(String.format(ACTION_HANDLING_ERROR, action.title(), PAYLOAD_EMPTY_ERROR));
        }

        if (enquiryInformationRequestEvent.getTransitOperation() == null || enquiryInformationRequestEvent.getTransitOperation().getMRN() == null) {
            throw new BaseException(String.format(ACTION_HANDLING_ERROR, action.title(), EMPTY_MESSAGE_MRN));
        }

        String movementId = enquiryInformationRequestEvent.getMovementId();

        if (movementId != null) {
            Movement movement = getMovementById(movementId);

            if (!DESTINATION.getValue().equals(movement.getOfficeRoleType())) {
                throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), MOVEMENT_BELONGS_TO_WRONG_OFFICE_ROLE_TYPE));
            }
        }

        CD145CType cd145CType = new CD145CType();
        cd145CType.setTransitOperation(enquiryInformationRequestEvent.getTransitOperation());
        cd145CType.setCompetentCustomsOfficeAtDeparture(enquiryInformationRequestEvent.getCompetentCustomsOfficeAtDeparture());
        cd145CType.setCompetentCustomsOfficeAtDestination(enquiryInformationRequestEvent.getCompetentCustomsOfficeAtDestination());
        cd145CType.setEnquiry(enquiryInformationRequestEvent.getEnquiry());
        cd145CType.getRequestedInformation().addAll(enquiryInformationRequestEvent.getRequestedInformation());

        cd145CType.setMessageSender(messageHelperService.generateMessageSender());
        cd145CType.setMessageRecipient(generateMessageRecipient(cd145CType.getTransitOperation().getMRN()));
        cd145CType.setMessageType(MessageTypes.CD_145_C);
        cd145CType.setMessageIdentification(MessageUtil.generateMessageIdentification());
        cd145CType.setPreparationDateAndTime(LocalDateTime.now());

        Message message = setUpActionMessage(action);
        message = dossierDataService.persistMessage(message);
        validateMovementMessage(message);

        EnquiryAdditionalInformation additionalInformation = EnquiryAdditionalInformation.builder()
                                                                                         .mrn(enquiryInformationRequestEvent.getTransitOperation().getMRN())
                                                                                         .messageType(MessageTypes.CD_145_C.value())
                                                                                         .requester(enquiryInformationRequestEvent
                                                                                                 .getCompetentCustomsOfficeAtDeparture().getReferenceNumber())
                                                                                         .respondent(enquiryInformationRequestEvent
                                                                                                 .getCompetentCustomsOfficeAtDestination().getReferenceNumber())
                                                                                         .officeRoleType(DESTINATION.getValue())
                                                                                         .payload(XmlConverter.marshal(cd145CType,
                                                                                                 NamespacesEnum.NTA.getValue(),
                                                                                                 MessageTypes.CD_145_C.value()))
                                                                                         .build();

        dossierDataService.saveEnquiryAdditionalInformation(additionalInformation);

        if (movementId != null) {
            dossierDataService.associateMessageWithMovement(message.getId(), movementId);

            Message taxudMessage = Message.builder()
                                          .messageIdentification(cd145CType.getMessageIdentification())
                                          .messageType(MessageTypes.CD_145_C.value())
                                          .source(cd145CType.getMessageSender())
                                          .destination(cd145CType.getMessageRecipient())
                                          .payload(XmlConverter.marshal(cd145CType))
                                          .domain(COMMON.name())
                                          .direction(DirectionEnum.SENT.getDirection())
                                          .build();

            taxudMessage = dossierDataService.persistMessage(taxudMessage);
            dossierDataService.associateMessageWithMovement(taxudMessage.getId(), movementId);
        }

        ccnMessageProducer.publish(movementId != null ? movementId : additionalInformation.getMrn(), additionalInformation.getPayload(), MessageTypes.CD_145_C);
        log.debug("...handleEnquiryInformationRequest()");
    }

    private void handleAcceptGuarantee(AcceptGuaranteeAction action) throws BadRequestException, JsonProcessingException, IllegalArgumentException {
        if (Objects.isNull(action.getPayload())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), PAYLOAD_EMPTY_ERROR));
        }
        if (StringUtils.isBlank(action.getPayload().getMovementId())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), MOVEMENT_ID_EMPTY_ERROR));
        }
        if (action.getPayload().getGuarantee().isEmpty()) {
            Violation violation = Violation.builder().reason("No guarantees provided").build();
            throw new BadRequestException(List.of(violation));
        }

        Movement movement = dossierDataService.getMovementById(action.getPayload().getMovementId(), false);
        validateMovementStatus(movement, ACCEPT_GUARANTEE_USAGE_OFFICE_ROLE_TYLE_STATUSES_LIST);

        CodeList guaranteeTypesWRequiredReferenceCL =
                referenceDataAdapter.getCodeList(CodelistKeyEnum.GUARANTEE_TYPE_WITH_REFERENCE.getKey(), CodeListDomain.NCTSP5);
        Set<String> guaranteeTypesToCheck =
                guaranteeTypesWRequiredReferenceCL.getItems().stream().map(CodeListItem::getCode).collect(Collectors.toSet());
        for (GuaranteeType02 guarantee : action.getPayload().getGuarantee()) {
            if (guaranteeTypesToCheck.contains(guarantee.getGuaranteeType())
                    && guarantee.getGuaranteeReference()
                                .stream()
                                .anyMatch(
                                        guaranteeReference ->
                                                Objects.isNull(
                                                        guaranteeReference.getAmountToBeCovered())
                                                        || StringUtils.isEmpty(
                                                        guaranteeReference.getCurrency()))) {
                Violation violation = Violation.builder().reason("Guarantee data are incomplete").build();
                throw new BadRequestException(List.of(violation));
            }
        }

        Message message = dossierDataService.persistMessage(setUpActionMessage(action));
        validateMovementMessage(message);

        dossierDataService.associateMessageWithMovement(message.getId(), movement.getId());
        workTaskManagerClient.completeWorkTask(createWorkTaskCompletionData(action.getPayload().getWorkTaskId(), Map.of(
                WorkTaskPropertyKeysEnum.MESSAGE_ID.getPropertyKey(), message.getId()
        )));
    }

    private void handleCDMessageManagement(CDMessageManagementAction action) throws BadRequestException {
        try {

            Class<?> messageTypeClass = Class.forName(DDNTA_MESSAGES_PACKAGE + action.getPayload().getMessageType() + "Type");

            Message savedActionMessage = dossierDataService.persistMessage(setUpActionMessage(action));
            dossierDataService.associateMessageWithMovement(savedActionMessage.getId(), action.getPayload().getMovementId());

            String newMessageIdentification = MessageUtil.generateMessageIdentification();

            Object obj = XmlConverter.buildMessageFromXML(action.getPayload().getMessagePayload(), messageTypeClass);

            Field messageIdentification = obj.getClass().getDeclaredField("messageIdentification");
            Field preparationDateAndTime = obj.getClass().getDeclaredField("preparationDateAndTime");
            Field messageSenderField = obj.getClass().getDeclaredField("messageSender");
            Field messageRecipientField = obj.getClass().getDeclaredField("messageRecipient");

            messageIdentification.setAccessible(true);
            preparationDateAndTime.setAccessible(true);
            messageSenderField.setAccessible(true);
            messageRecipientField.setAccessible(true);

            messageIdentification.set(obj, newMessageIdentification);
            preparationDateAndTime.set(obj, LocalDateTime.now(ZoneOffset.UTC));
            String messageSender = (String) messageSenderField.get(obj);
            String messageRecipient = (String) messageRecipientField.get(obj);

            String newMessage = XmlConverter.marshal(obj, NamespacesEnum.NTA.getValue(), action.getPayload().getMessageType());

            Message savedMessage = dossierDataService.persistMessage(Message.builder()
                                                                            .messageIdentification(newMessageIdentification)
                                                                            .messageType(action.getPayload().getMessageType())
                                                                            .payload(newMessage)
                                                                            .direction(DirectionEnum.SENT.getDirection())
                                                                            .domain(COMMON.name())
                                                                            .manualInserted(false)
                                                                            .source(messageSender)
                                                                            .destination(messageRecipient)
                                                                            .build());
            validateMovementMessage(savedMessage);

            dossierDataService.associateMessageWithMovement(savedMessage.getId(), action.getPayload().getMovementId());
            ccnMessageProducer.publish(action.getPayload().getMovementId() != null ? action.getPayload().getMovementId() : action.getPayload().getMrn(),
                    newMessage,
                    MessageTypes.fromValue(action.getPayload().getMessageType()));

            dossierDataService.updateRejectedMessage(true, action.getPayload().getRejectedMessageId());
        } catch (ClassNotFoundException e) {
            throw new BaseException("Couldn't find specified class", e);
        } catch (JsonProcessingException e) {
            throw new BaseException("Error setting up action message ", e);
        } catch (JAXBException e) {
            throw new BaseException("Error converting string xml to object", e);
        } catch (NoSuchFieldException e) {
            throw new BaseException("No such field on object", e);
        } catch (IllegalAccessException e) {
            throw new BaseException("Error setting field value", e);
        }
    }

    private void handleRejectGuarantee(RejectGuaranteeAction action) throws BadRequestException, JsonProcessingException, IllegalArgumentException {
        if (Objects.isNull(action.getPayload())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), PAYLOAD_EMPTY_ERROR));
        }
        if (StringUtils.isBlank(action.getPayload().getMovementId())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), MOVEMENT_ID_EMPTY_ERROR));
        }
        if (action.getPayload().getGuarantee().isEmpty()
                && action.getPayload().getGuaranteeNonGMS().isEmpty()) {
            Violation violation = Violation.builder().reason("No guarantees provided").build();
            throw new BadRequestException(List.of(violation));
        }

        Movement movement = dossierDataService.getMovementById(action.getPayload().getMovementId(), false);
        validateMovementStatus(movement, REJECT_GUARANTEE_USAGE_OFFICE_ROLE_TYLE_STATUSES_LIST);

        Message message = dossierDataService.persistMessage(setUpActionMessage(action));
        validateMovementMessage(message);

        dossierDataService.associateMessageWithMovement(message.getId(), movement.getId());
        workTaskManagerClient.completeWorkTask(createWorkTaskCompletionData(action.getPayload().getWorkTaskId(), Map.of(
                WorkTaskPropertyKeysEnum.MESSAGE_ID.getPropertyKey(), message.getId()
        )));
    }

    private void handleSealsAcknowledgement(SealsAcknowledgementAction action) throws JsonProcessingException {

        Movement movement = dossierDataService.getMovementById(action.getPayload().getMovementId(), false);
        validateMovementStatus(movement, SEALS_ACKNOWLEDGEMENT_STATUSES_LIST);

        Message message = dossierDataService.persistMessage(setUpActionMessage(action));

        dossierDataService.associateMessageWithMovement(message.getId(), movement.getId());

    }

    private void setDataToCC140CMessage(CC140CType cc140CType) {
        cc140CType.setMessageSender(messageHelperService.generateMessageSender());
        cc140CType.setMessageType(MessageTypes.CC_140_C);
        cc140CType.setMessageIdentification(MessageUtil.generateMessageIdentification());
        cc140CType.setPreparationDateAndTime(LocalDateTime.now());
        cc140CType.setMessageRecipient("swp.transit.agent");
    }

    private void setDataToCC141CMessage(CC141CType cc141CType) {
        cc141CType.setMessageSender(messageHelperService.generateMessageSender());
        cc141CType.setMessageType(MessageTypes.CC_141_C);
        cc141CType.setMessageIdentification(MessageUtil.generateMessageIdentification());
        cc141CType.setPreparationDateAndTime(LocalDateTime.now());
    }

    private void setDataToCD142CMessage(CD142CType cd142CType) {
        cd142CType.setMessageSender(messageHelperService.generateMessageSender());
        cd142CType.setMessageType(MessageTypes.CD_142_C);
        cd142CType.setMessageIdentification(MessageUtil.generateMessageIdentification());
        cd142CType.setPreparationDateAndTime(LocalDateTime.now(ZoneOffset.UTC));
        cd142CType.setMessageRecipient("NTA." + cd142CType.getCustomsOfficeOfRequest().getReferenceNumber().substring(0, 2));
    }

    private void setDataToCD143CMessage(CD143CType cd143CType) {
        cd143CType.setMessageSender(messageHelperService.generateMessageSender());
        cd143CType.setMessageType(MessageTypes.CD_143_C);
        cd143CType.setMessageIdentification(MessageUtil.generateMessageIdentification());
        cd143CType.setPreparationDateAndTime(LocalDateTime.now());
    }

    private String generateMessageRecipient(String mrn) {
        return NTA + mrn.substring(2, 4);
    }

    private String generateMessageRecipientFromOfficeId(String officeId) {
        return NTA.concat(officeId.substring(0, 2));
    }

    private Movement setUpMovement(String mrn, String officeRoleType, String officeId) {
        return Movement.builder()
                       .officeRoleType(officeRoleType)
                       .officeId(officeId)
                       .mrn(mrn)
                       .build();
    }

    public <T> Message setUpActionMessage(Action<T> action) throws JsonProcessingException {
        return Message.builder()
                      .messageIdentification(MessageUtil.generateMessageIdentification())
                      .messageType(getMessageType(action))
                      .source(userDetailsService.getUsername())
                      .destination(messageHelperService.generateMessageSender())
                      .payload(objectMapper.writeValueAsString(action.getPayload()))
                      .domain(MessageDomainTypeEnum.ERMIS.name())
                      .direction(DirectionEnum.RECEIVED.getDirection())
                      .build();
    }

    public <T> Message setUpActionMessage(Action<T> action, String messageId, String source, String destination) throws JsonProcessingException {
        return Message.builder()
                      .messageIdentification(messageId)
                      .messageType(getMessageType(action))
                      .source(source)
                      .destination(destination)
                      .payload(objectMapper.writeValueAsString(action.getPayload()))
                      .domain(MessageDomainTypeEnum.ERMIS.name())
                      .direction(DirectionEnum.RECEIVED.getDirection())
                      .build();
    }

    private <T> String getMessageType(Action<T> action) {
        return getMessageType(action.getPayload());
    }

    private <T> String getMessageType(T actionPayload) {
        Class<T> payloadType = (Class<T>) actionPayload.getClass();
        if (payloadType.getSimpleName().startsWith("CD") || payloadType.getSimpleName().startsWith("CC")) {
            return payloadType.getSimpleName().replace("Type", "");
        } else {
            return payloadType.getSimpleName();
        }
    }

    private Predicate<WorkTask> getWorkTaskPredicate(OfficeRoleTypesEnum officeRole, ActionTypeEnum action) throws UnsupportedOperationException {
        return o -> {
            Optional<?> v;
            switch (officeRole) {
                case DEPARTURE:
                    v = Optional.ofNullable(OfficeOfDepartureWorkTaskTypeEnum.of(o.getType()));
                    break;
                case DESTINATION:
                    v = Optional.ofNullable(OfficeOfDestinationWorkTaskTypeEnum.of(o.getType()));
                    break;
                case EXIT:
                    v = Optional.ofNullable(OfficeOfExitForTransitWorkTaskTypeEnum.of(o.getType()));
                    break;
                case TRANSIT:
                    v = Optional.ofNullable(OfficeOfTransitWorkTaskTypeEnum.of(o.getType()));
                    break;
                case INCIDENT:
                    v = Optional.ofNullable(OfficeOfIncidentWorkTaskTypeEnum.of(o.getType()));
                    break;
                case LODGEMENT:
                    v = Optional.ofNullable(OfficeOfLodgementWorkTaskTypeEnum.of(o.getType()));
                    break;
                default:
                    throw new UnsupportedOperationException();
            }

            if (v.isPresent()) {
                return v.get().equals(action.getWorkTaskType());
            } else {
                log.error(String.format("Unknown work task type \"%s\" for office role \"%s\"", o.getType(), officeRole.getValue()));
                return false;
            }
        };
    }

    // TODO eventually this should be moved to MessageValidationService and delete current one
    private void validateMovementStatus(Movement movement, List<OfficeRoleTypeStatuses> officeRoleTypeStatusesList) throws IllegalArgumentException {
        // TODO: Convert to OutOfOrder violation exception type
        if (!MessageValidationUtils.isStateValid(movement, officeRoleTypeStatusesList)) {
            log.error("Found not valid status: '{}' for movement with id: '{}'", movement.getProcessingStatus(),
                    movement.getId());
            throw new IllegalArgumentException("Movement status is invalid for this action");
        }
    }

    private void validateMovementMessage(Message message) throws BadRequestException {
        validate(message, validationService::validateMovementMessageAllValidations);
    }

    private void validate(Message message, Function<String, List<Violation>> validateFunction) throws BadRequestException {
        List<Violation> violations = validateFunction.apply(message.getId());

        if (!CollectionUtils.isEmpty(violations)) {
            log.error("validate(): Via ValidationService -> Found [{}] violation(s) on message: [{}, {}]", violations.size(), message.getMessageType(),
                    message.getMessageIdentification());
            violations.forEach(violation -> log.error("\t -> " + violation.toString()));
            dossierDataService.updateMessage(message.getId(), MessageStatusEnum.REJECTED.name());
            throw new BadRequestException(violations);
        } else {
            dossierDataService.updateMessage(message.getId(), MessageStatusEnum.ACCEPTED.name());
        }

    }

    private Movement getMovementById(String movementId) {
        Movement movement;
        try {
            movement = dossierDataService.getMovementById(movementId, false);
        } catch (Exception e) {
            throw new IllegalArgumentException(MOVEMENT_NOT_FOUND);
        }
        return movement;
    }

    @SneakyThrows
    private CompleteWorkTaskDTO getCompleteWorkTaskDTOFromAction(Action action) {
        CompleteWorkTaskDTO completeWorkTaskDTO = ((WorkTaskAction) action).getPayload();
        if (Objects.isNull(completeWorkTaskDTO)) {
            throw new BadRequestException(List.of(Violation.builder().reason("No workTask completion data provided").build()));
        }
        completeWorkTaskDTO.setSubmitter(userDetailsService.getUsername());
        return completeWorkTaskDTO;
    }

    private boolean checkMovementNotExists(String mrn, OfficeRoleTypesEnum officeRole, String officeId, String lrn) {
        try {
            dossierDataService.findSingleMovementByCriteria(mrn, officeRole.getValue(), officeId, lrn);
        } catch (Exception e) {
            // This SHOULD happen as the movement should not exist.
            return true;
        }
        return false;
    }

    private CD027CType createCD027CType(Action<?> action, Supplier<String> mrnSupplier, Supplier<CustomsOfficeOfRequestType01> customsOfficeSupplier) {
        if (Objects.isNull(action.getPayload())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), PAYLOAD_EMPTY_ERROR));
        }
        if (StringUtils.isBlank(mrnSupplier.get())) {
            throw new IllegalArgumentException(String.format(ACTION_HANDLING_ERROR, action.title(), MRN_EMPTY_ERROR));
        }

        CD027CType cd027CType = new CD027CType();
        TransitOperationType43 transitOperationType43 = new TransitOperationType43();
        transitOperationType43.setMRN(mrnSupplier.get());
        cd027CType.setTransitOperation(transitOperationType43);
        cd027CType.setCustomsOfficeOfRequest(customsOfficeSupplier.get());

        cd027CType.setMessageSender(messageHelperService.generateMessageSender());
        cd027CType.setMessageRecipient(generateMessageRecipient(cd027CType.getTransitOperation().getMRN()));
        cd027CType.setMessageType(MessageTypes.CD_027_C);
        cd027CType.setMessageIdentification(MessageUtil.generateMessageIdentification());
        cd027CType.setPreparationDateAndTime(LocalDateTime.now(ZoneOffset.UTC));

        return cd027CType;
    }

    private Map<String, Message> genericMovementQueryHandle(CD027CType cd027CType, boolean isGlobalEvent, Action<?> action)
            throws JsonProcessingException, BadRequestException {

        Message actionMessage = isGlobalEvent ?
                setUpActionMessage(action, cd027CType.getMessageIdentification(), cd027CType.getMessageSender(), cd027CType.getMessageRecipient()) :
                setUpActionMessage(action);

        actionMessage = dossierDataService.persistMessage(actionMessage);

        validateMovementMessage(actionMessage);

        String cd027cTypeMessageXML = XmlConverter.marshal(cd027CType, NamespacesEnum.NTA.getValue(), MessageTypes.CD_027_C.value());

        Message cD027CMessage = Message.builder()
                                       .messageIdentification(cd027CType.getMessageIdentification())
                                       .messageType(cd027CType.getMessageType().value())
                                       .source(cd027CType.getMessageSender())
                                       .destination(cd027CType.getMessageRecipient())
                                       .domain(COMMON.name())
                                       .payload(cd027cTypeMessageXML)
                                       .direction(DirectionEnum.SENT.getDirection())
                                       .build();

        cD027CMessage = dossierDataService.persistMessage(cD027CMessage);

        createMovementQuery(cd027CType, cd027cTypeMessageXML);

        return Map.of(ACTION_MESSAGE, actionMessage, CD027C_MESSAGE, cD027CMessage);
    }

    private void handleTerminateTIRMovement(TerminateTIRMovementAction action) throws JsonProcessingException, IllegalArgumentException {
        // Fetch related Movement:
        Movement movement = dossierDataService.getMovementById(action.getPayload().getMovementId(), false);
        // Validate Movement status:
        validateMovementStatus(movement, ValidationConstants.TERMINATE_TIR_MOVEMENT_STATUS_LIST);
        // If valid, persist the action Message:
        Message message = dossierDataService.persistMessage(setUpActionMessage(action));
        // Associate with Movement - this is done for action tracking and confirmation of conditions for availability of said action:
        dossierDataService.associateMessageWithMovement(message.getId(), action.getPayload().getMovementId());
        // Publish Message command towards Destination Office:
        processMessageCommandProducer.publish(message.getId(), TerminateTIRMovementEvent.class.getSimpleName(), action.getPayload().getMovementId(),
                OfficeRoleTypesEnum.DESTINATION.getValue());
    }
}
package com.intrasoft.ermis.transit.management.core.actions;

import static com.intrasoft.ermis.ddntav5152.messages.OfficeRoleType.INCIDENT;
import static com.intrasoft.ermis.transit.shared.testing.util.FileHelpers.xmlToString;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.intrasoft.ermis.common.logging.ErmisLogger;
import com.intrasoft.ermis.common.xml.parsing.XmlConverter;
import com.intrasoft.ermis.ddntav5152.messages.CD027CType;
import com.intrasoft.ermis.ddntav5152.messages.CD144CType;
import com.intrasoft.ermis.ddntav5152.messages.CD145CType;
import com.intrasoft.ermis.ddntav5152.messages.CD180CType;
import com.intrasoft.ermis.ddntav5152.messages.EnquiryInformationEvent;
import com.intrasoft.ermis.ddntav5152.messages.EnquiryInformationRequestEvent;
import com.intrasoft.ermis.ddntav5152.messages.IncidentNotificationDepEvent;
import com.intrasoft.ermis.ddntav5152.messages.IncidentNotificationDestEvent;
import com.intrasoft.ermis.ddntav5152.messages.IncidentNotificationEvent;
import com.intrasoft.ermis.ddntav5152.messages.IncidentNotificationTrsEvent;
import com.intrasoft.ermis.ddntav5152.messages.IncidentNotificationTrxsEvent;
import com.intrasoft.ermis.ddntav5152.messages.MessageTypes;
import com.intrasoft.ermis.ddntav5152.messages.MovementQueryGlobalEvent;
import com.intrasoft.ermis.ddntav5152.messages.MovementQueryIncidentEvent;
import com.intrasoft.ermis.platform.shared.client.reference.data.core.ReferenceDataAdapter;
import com.intrasoft.ermis.platform.shared.client.reference.data.core.domain.CodeListDomain;
import com.intrasoft.ermis.transit.common.enums.ActionTypeEnum;
import com.intrasoft.ermis.transit.common.enums.OfficeOfDestinationWorkTaskTypeEnum;
import com.intrasoft.ermis.transit.common.enums.OfficeRoleTypesEnum;
import com.intrasoft.ermis.transit.common.enums.ProcessingStatusTypeEnum;
import com.intrasoft.ermis.transit.common.exceptions.NotFoundException;
import com.intrasoft.ermis.transit.common.service.MessageHelperService;
import com.intrasoft.ermis.transit.management.core.FileUtil;
import com.intrasoft.ermis.transit.management.core.action.base.ActionFactory;
import com.intrasoft.ermis.transit.management.core.action.conditions.AmendCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.AmendPaperCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.CHHandleErrorsCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.CHHandleFailuresCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.CancelEnquiryProcedureCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.CancelPaperCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.CancellationOnDemandCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.CaptureResultsOfControlCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.CargoManualArrivalForMovementCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.CompleteRecoveryDepCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.CompleteRecoveryDestCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.CompleteRecoveryExitCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.CompleteRecoveryTrsCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.ControlDecisionAfterUnloadingCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.ControlDecisionNotSimplifiedCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.ControlDecisionSimplifiedCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.GuaranteeIntegrityCheckCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.InsertDestinationControlResultsCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.IntentionToControlCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.InvalidationDecisionCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.MLSACondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.ManualATRCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.ManualAcceptanceCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.ManualArrivalForMovementCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.ManualControlDecisionCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.ManualControlDecisionDestinationCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.ManualControlDecisionNotificationCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.ManualNoControlDecisionCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.ManualReleaseCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.ManualTraderEnquiryResponseCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.ManualUnloadingRemarksCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.PostponeEnquiryCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.ProceedWithRecoveryCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.RecommendEnquiryCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.RecommendRecoveryCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.RecoveryRequestDestForMovementCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.RecoveryRequestIncForMovementCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.RecoveryRequestToOtherCountryCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.RecoveryRequestTrsForMovementCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.RecoveryRequestTrxsForMovementCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.RecoveryResponseDepCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.RecoveryResponseDestCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.RecoveryResponseExitCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.RecoveryResponseIncidentCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.RecoveryResponseTrsCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.RegisterControlResultsCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.RegisterIncidentCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.RequestAdditionalInformationCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.RequestTraderEnquiryCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.ResolveDiscrepanciesCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.ReviewUnloadingRemarksCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.StatusRequestCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.TerminateTIRMovementCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.UpdateGuaranteeOfficeCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.UpdateSealsCondition;
import com.intrasoft.ermis.transit.management.core.action.conditions.base.ActionFilterPredicateEvaluator;
import com.intrasoft.ermis.transit.management.core.port.outbound.CcnMessageProducer;
import com.intrasoft.ermis.transit.management.core.port.outbound.DossierDataService;
import com.intrasoft.ermis.transit.management.core.port.outbound.ProcessMessageCommandProducer;
import com.intrasoft.ermis.transit.management.core.port.outbound.ValidationService;
import com.intrasoft.ermis.transit.management.core.port.outbound.WorkTaskManagerClient;
import com.intrasoft.ermis.transit.management.core.service.ActionServiceImpl;
import com.intrasoft.ermis.transit.management.domain.Action;
import com.intrasoft.ermis.transit.management.domain.Declaration;
import com.intrasoft.ermis.transit.management.domain.Message;
import com.intrasoft.ermis.transit.management.domain.Movement;
import com.intrasoft.ermis.transit.management.domain.WorkTask;
import com.intrasoft.ermis.transit.shared.security.service.UserDetailsService;
import com.intrasoft.proddev.referencedata.shared.enums.CodelistKeyEnum;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
import javax.xml.bind.JAXBException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;


@ExtendWith(MockitoExtension.class)
class ActionServiceImplUT {

    @Mock
    private ErmisLogger ermisLogger;

    @Mock
    private DossierDataService dossierDataService;

    @Mock
    private WorkTaskManagerClient workTaskManagerClient;

    @Mock
    private ValidationService validationService;

    @Mock
    private ProcessMessageCommandProducer processMessageCommandProducer;

    @Mock
    private MessageHelperService messageHelperService;

    @Mock
    private ObjectMapper objectMapper;

    @Mock
    private CcnMessageProducer ccnMessageProducer;

    @Mock
    private UserDetailsService userDetailsService;

    @Mock
    private ReferenceDataAdapter referenceDataAdapter;

    @Captor
    private ArgumentCaptor<Message> messageArgumentCaptor;

    @Captor
    private ArgumentCaptor<String> messageIdCaptor;

    @Captor
    private ArgumentCaptor<String> messageTypeCaptor;

    @InjectMocks
    private ActionServiceImpl actionService;

    private static final String TEST_MOVEMENT_ID = "test";

    private static final String OFFICE_ID = "DK004600";

    @BeforeEach
    void init() {
        Map<ActionTypeEnum, ActionFilterPredicateEvaluator> evaluatorMap = this.actionService.getActionEvaluatorMap();
        evaluatorMap.put(ActionTypeEnum.AMEND, new AmendCondition());
        evaluatorMap.put(ActionTypeEnum.CANCEL_ENQUIRY_PROCEDURE, new CancelEnquiryProcedureCondition());
        evaluatorMap.put(ActionTypeEnum.CANCELLATION_ON_DEMAND, new CancellationOnDemandCondition());
        evaluatorMap.put(ActionTypeEnum.CAPTURE_RESULTS_OF_CONTROL, new CaptureResultsOfControlCondition());
        evaluatorMap.put(ActionTypeEnum.COMPLETE_RECOVERY_DEP, new CompleteRecoveryDepCondition());
        evaluatorMap.put(ActionTypeEnum.COMPLETE_RECOVERY_DEST, new CompleteRecoveryDestCondition());
        evaluatorMap.put(ActionTypeEnum.COMPLETE_RECOVERY_EXIT, new CompleteRecoveryExitCondition());
        evaluatorMap.put(ActionTypeEnum.COMPLETE_RECOVERY_TRS, new CompleteRecoveryTrsCondition());
        evaluatorMap.put(ActionTypeEnum.CONTROL_DECISION_AFTER_UNLOADING, new ControlDecisionAfterUnloadingCondition());
        evaluatorMap.put(ActionTypeEnum.CONTROL_DECISION_NOT_SIMPLIFIED, new ControlDecisionNotSimplifiedCondition());
        evaluatorMap.put(ActionTypeEnum.CONTROL_DECISION_SIMPLIFIED, new ControlDecisionSimplifiedCondition());
        evaluatorMap.put(ActionTypeEnum.GUARANTEE_INTEGRITY_CHECK, new GuaranteeIntegrityCheckCondition());
        evaluatorMap.put(ActionTypeEnum.INSERT_DESTINATION_CONTROL_RESULTS, new InsertDestinationControlResultsCondition());
        evaluatorMap.put(ActionTypeEnum.INTENTION_TO_CONTROL, new IntentionToControlCondition());
        evaluatorMap.put(ActionTypeEnum.INVALIDATION_DECISION, new InvalidationDecisionCondition());
        evaluatorMap.put(ActionTypeEnum.MANUAL_ACCEPTANCE, new ManualAcceptanceCondition());
        evaluatorMap.put(ActionTypeEnum.MANUAL_ARRIVAL_FOR_MOVEMENT, new ManualArrivalForMovementCondition());
        evaluatorMap.put(ActionTypeEnum.MANUAL_ATR, new ManualATRCondition());
        evaluatorMap.put(ActionTypeEnum.MANUAL_CONTROL_DECISION, new ManualControlDecisionCondition());
        evaluatorMap.put(ActionTypeEnum.MANUAL_RELEASE, new ManualReleaseCondition());
        evaluatorMap.put(ActionTypeEnum.MLSA, new MLSACondition());
        evaluatorMap.put(ActionTypeEnum.POSTPONE_ENQUIRY, new PostponeEnquiryCondition());
        evaluatorMap.put(ActionTypeEnum.PROCEED_WITH_RECOVERY, new ProceedWithRecoveryCondition());
        evaluatorMap.put(ActionTypeEnum.RECOMMEND_ENQUIRY, new RecommendEnquiryCondition());
        evaluatorMap.put(ActionTypeEnum.RECOMMEND_RECOVERY, new RecommendRecoveryCondition());
        evaluatorMap.put(ActionTypeEnum.RECOVERY_REQUEST_TO_OTHER_COUNTRY, new RecoveryRequestToOtherCountryCondition());
        evaluatorMap.put(ActionTypeEnum.REGISTER_CONTROL_RESULTS, new RegisterControlResultsCondition());
        evaluatorMap.put(ActionTypeEnum.REGISTER_INCIDENT, new RegisterIncidentCondition());
        evaluatorMap.put(ActionTypeEnum.REQUEST_TRADER_ENQUIRY, new RequestTraderEnquiryCondition());
        evaluatorMap.put(ActionTypeEnum.RESOLVE_DISCREPANCIES, new ResolveDiscrepanciesCondition());
        evaluatorMap.put(ActionTypeEnum.REVIEW_UNLOADING_REMARKS, new ReviewUnloadingRemarksCondition());
        evaluatorMap.put(ActionTypeEnum.STATUS_REQUEST, new StatusRequestCondition());
        evaluatorMap.put(ActionTypeEnum.UPDATE_GUARANTEE_OFFICE, new UpdateGuaranteeOfficeCondition());
        evaluatorMap.put(ActionTypeEnum.REQUEST_ADDITIONAL_INFORMATION, new RequestAdditionalInformationCondition());
        evaluatorMap.put(ActionTypeEnum.RECOVERY_RESPONSE_DEP, new RecoveryResponseDepCondition());
        evaluatorMap.put(ActionTypeEnum.RECOVERY_RESPONSE_DEST, new RecoveryResponseDestCondition());
        evaluatorMap.put(ActionTypeEnum.RECOVERY_RESPONSE_EXIT, new RecoveryResponseExitCondition());
        evaluatorMap.put(ActionTypeEnum.RECOVERY_RESPONSE_TRS, new RecoveryResponseTrsCondition());
        evaluatorMap.put(ActionTypeEnum.RECOVERY_REQUEST_DEST_FOR_MOVEMENT, new RecoveryRequestDestForMovementCondition());
        evaluatorMap.put(ActionTypeEnum.RECOVERY_REQUEST_TRS_FOR_MOVEMENT, new RecoveryRequestTrsForMovementCondition());
        evaluatorMap.put(ActionTypeEnum.RECOVERY_REQUEST_TRXS_FOR_MOVEMENT, new RecoveryRequestTrxsForMovementCondition());
        evaluatorMap.put(ActionTypeEnum.RECOVERY_REQUEST_INC_FOR_MOVEMENT, new RecoveryRequestIncForMovementCondition());
        evaluatorMap.put(ActionTypeEnum.RECOVERY_RESPONSE_INCIDENT, new RecoveryResponseIncidentCondition());
        evaluatorMap.put(ActionTypeEnum.REQUEST_OFFICE_ENQUIRY, new RequestTraderEnquiryCondition());
        evaluatorMap.put(ActionTypeEnum.MANUAL_CONTROL_DECISION_NOTIFICATION, new ManualControlDecisionNotificationCondition());
        evaluatorMap.put(ActionTypeEnum.MANUAL_TRADER_ENQUIRY_RESPONSE, new ManualTraderEnquiryResponseCondition());
        evaluatorMap.put(ActionTypeEnum.AMEND_PAPER, new AmendPaperCondition());
        evaluatorMap.put(ActionTypeEnum.CANCEL_PAPER, new CancelPaperCondition());
        evaluatorMap.put(ActionTypeEnum.SEALS_ACKNOWLEDGEMENT, new ManualArrivalForMovementCondition());
        evaluatorMap.put(ActionTypeEnum.UPDATE_SEALS, new UpdateSealsCondition());
        evaluatorMap.put(ActionTypeEnum.MANUAL_CONTROL_DECISION_DESTINATION, new ManualControlDecisionDestinationCondition());
        evaluatorMap.put(ActionTypeEnum.MANUAL_NO_CONTROL_DECISION, new ManualNoControlDecisionCondition());
        evaluatorMap.put(ActionTypeEnum.CARGO_MANUAL_ARRIVAL_FOR_MOVEMENT, new CargoManualArrivalForMovementCondition());
        evaluatorMap.put(ActionTypeEnum.CH_HANDLE_ERRORS, new CHHandleErrorsCondition());
        evaluatorMap.put(ActionTypeEnum.CH_HANDLE_TECHNICAL_ERRORS, new CHHandleFailuresCondition());
        evaluatorMap.put(ActionTypeEnum.MANUAL_UNLOADING_REMARKS, new ManualUnloadingRemarksCondition());
        evaluatorMap.put(ActionTypeEnum.TERMINATE_TIR_MOVEMENT, new TerminateTIRMovementCondition());
    }

    @ParameterizedTest
    @MethodSource("getActionFilteringCases")
    void testActionFiltering(OfficeRoleTypesEnum officeRole, String officeId, List<String> userRoles, ProcessingStatusTypeEnum movementStatus,
            List<WorkTask> openWorkTasks, List<Action> expectedAvailableGlobalActions, List<Action> expectedAvailableMovementActions) {
        Optional<Movement> movement = createMovement(officeRole, movementStatus);

        if (movement.isPresent()) {
            // The only reason the movement should not be found is if the movementId is null during the API call
            when(dossierDataService.getMovementById(TEST_MOVEMENT_ID, false)).thenReturn(movement.get());
            if (OfficeRoleTypesEnum.DEPARTURE.getValue().equals(movement.get().getOfficeRoleType())) {
                when(referenceDataAdapter.getCodeListItemAttributeValueAsString(any(), any(), any(), any(), any())).thenReturn(OFFICE_ID);
            } else if (OfficeRoleTypesEnum.TRANSIT.getValue().equals(movement.get().getOfficeRoleType())
                    || OfficeRoleTypesEnum.EXIT.getValue().equals(movement.get().getOfficeRoleType())) {
                when(dossierDataService.getDeclaration(any())).thenReturn(Declaration
                        .builder()
                        .payload(FileUtil.loadTextFile(MessageTypes.CD_050_C.value(), null))
                        .build());
                when(referenceDataAdapter.verifyCodeListItem(CodelistKeyEnum.CUSTOMS_OFFICE_RECOVERY.getKey(), OFFICE_ID, CodeListDomain.NCTSP5)).thenReturn(
                        true);

            } else if (OfficeRoleTypesEnum.DESTINATION.getValue().equals(movement.get().getOfficeRoleType())) {
                when(referenceDataAdapter.verifyCodeListItem(CodelistKeyEnum.CUSTOMS_OFFICE_RECOVERY.getKey(), OFFICE_ID, CodeListDomain.NCTSP5)).thenReturn(
                        true);
            }
        }
        if (movementStatus != null) {
            when(workTaskManagerClient.findOpenWorkTasks(TEST_MOVEMENT_ID)).thenReturn(openWorkTasks);
        }

        List<Action> globalActions = actionService.getGlobalActions(officeRole, userRoles, officeId);
        System.out.println(globalActions);
        List<Action> movementActions = actionService.getMovementActions(officeRole, TEST_MOVEMENT_ID, userRoles, OFFICE_ID);
        System.out.println(movementActions);

        assertThat(globalActions).hasSameElementsAs(expectedAvailableGlobalActions);
        assertThat(movementActions).hasSameElementsAs(expectedAvailableMovementActions);
    }

    @ParameterizedTest
    @CsvSource({
            "INCIDENT_NOTIFICATION, INCIDENT, I03",
            "INCIDENT_NOTIFICATION_DEP, DEPARTURE, A01",
            "INCIDENT_NOTIFICATION_DEST, DESTINATION, C01",
            "INCIDENT_NOTIFICATION_TRXS, EXIT, M02",
            "INCIDENT_NOTIFICATION_TRS, TRANSIT, D01"
    })
    void testCompleteActionIncidentNotification(ActionTypeEnum actionTypeEnum, OfficeRoleTypesEnum officeRoleTypesEnum, String officeStatus)
            throws JAXBException, JsonProcessingException {

        // given
        String movementId = "ceb0cc2b-dd74-495b-87cb-6ab27c7687fd";
        String messageXml = xmlToString("CD180C.xml");
        CD180CType cd180CType = XmlConverter.buildMessageFromXML(messageXml, CD180CType.class);

        Action action = getAction(actionTypeEnum, movementId, cd180CType);

        // when
        Movement incidentMovement = createMovement(movementId, officeRoleTypesEnum.getValue(), "AK020053", "31DE061F60A83E16K1",
                "1112236201149", officeStatus);
        Message message = actionService.setUpActionMessage(action);

        when(dossierDataService.getMovementById(any(), anyBoolean())).thenReturn(incidentMovement);
        when(dossierDataService.persistMessage(any())).thenReturn(message);
        when(validationService.validateMovementMessageAllValidations(any())).thenReturn(Collections.emptyList());
        when(messageHelperService.generateMessageSender()).thenReturn("DK");
        doNothing().when(dossierDataService).associateMessageWithMovement(any(), any());
        doNothing().when(processMessageCommandProducer).publish(any(), any(), any(Movement.class));

        //verify
        assertDoesNotThrow(() -> actionService.completeAction(action));
        verify(dossierDataService, times(1)).persistMessage(messageArgumentCaptor.capture());
        verify(dossierDataService, times(1)).associateMessageWithMovement(messageIdCaptor.capture(), eq(movementId));
        verify(processMessageCommandProducer, times(1)).publish(messageIdCaptor.capture(), messageTypeCaptor.capture(),
                eq(incidentMovement));
    }

    @Test
    void testCompleteActionMovementQueryIncident() throws JAXBException, JsonProcessingException {
        // given
        String movementId = "ceb0cc2b-dd74-495b-87cb-6ab27c7687fd";
        String messageXml = xmlToString("CD027C.xml");
        CD027CType cd027CType = XmlConverter.buildMessageFromXML(messageXml, CD027CType.class);

        Action<MovementQueryIncidentEvent> action = ActionFactory.getAction(ActionTypeEnum.MOVEMENT_QUERY_INCIDENT);
        MovementQueryIncidentEvent movementQueryIncidentEvent = new MovementQueryIncidentEvent();
        movementQueryIncidentEvent.setMrn(cd027CType.getTransitOperation().getMRN());
        movementQueryIncidentEvent.setCustomsOfficeOfRequest(cd027CType.getCustomsOfficeOfRequest());
        action.setPayload(movementQueryIncidentEvent);

        // when
        Movement incidentMovement = createMovement(movementId, OfficeRoleTypesEnum.INCIDENT.getValue(), "AK020053", "22DKSCZ59GWFUVFIJ8",
                "1112236201149", ProcessingStatusTypeEnum.OOINC_MOVEMENT_QUERY_POSITIVE.getItemCode());
        Message message = actionService.setUpActionMessage(action);

        when(dossierDataService.persistMessage(any())).thenReturn(message);
        when(validationService.validateMovementMessageAllValidations(any())).thenReturn(Collections.emptyList());
        when(messageHelperService.generateMessageSender()).thenReturn("DK");
        when(dossierDataService.findSingleMovementByCriteria(eq(cd027CType.getTransitOperation().getMRN()), eq(INCIDENT.value()), any(), any()))
                .thenThrow(new NotFoundException("Movement not found"));
        when(dossierDataService.createMovement(any())).thenReturn(incidentMovement);
        doNothing().when(dossierDataService).associateMessageWithMovement(any(), any());
        doNothing().when(processMessageCommandProducer).publish(any(), any(), any(Movement.class));

        //verify
        assertDoesNotThrow(() -> actionService.completeAction(action));
        verify(dossierDataService, times(2)).persistMessage(messageArgumentCaptor.capture());
        verify(dossierDataService, times(2)).associateMessageWithMovement(messageIdCaptor.capture(), eq(incidentMovement.getId()));
        verify(processMessageCommandProducer, times(1)).publish(messageIdCaptor.capture(), messageTypeCaptor.capture(),
                eq(incidentMovement));
    }

    @Test
    void testCompleteActionMovementQueryGlobal() throws JAXBException, JsonProcessingException {

        // given
        String messageXml = xmlToString("CD027C.xml");
        CD027CType cd027CType = XmlConverter.buildMessageFromXML(messageXml, CD027CType.class);

        Action<MovementQueryGlobalEvent> action = ActionFactory.getAction(ActionTypeEnum.MOVEMENT_QUERY_GLOBAL);
        MovementQueryGlobalEvent movementQueryGlobalEvent = new MovementQueryGlobalEvent();
        movementQueryGlobalEvent.setMrn(cd027CType.getTransitOperation().getMRN());
        movementQueryGlobalEvent.setCustomsOfficeOfRequest(cd027CType.getCustomsOfficeOfRequest());
        action.setPayload(movementQueryGlobalEvent);

        // when
        Message message = actionService.setUpActionMessage(action);
        when(dossierDataService.persistMessage(any())).thenReturn(message);

        //verify
        assertDoesNotThrow(() -> actionService.completeAction(action));
        verify(dossierDataService, times(1)).saveMovementQuery(any());
        verify(ccnMessageProducer, times(1)).publish(any(), any(), any());
    }

    @Test
    void testCompleteActionEnquiryInformation() throws JAXBException, JsonProcessingException {

        // given
        String messageXml = xmlToString("CD144C.xml");
        CD144CType cd144CType = XmlConverter.buildMessageFromXML(messageXml, CD144CType.class);

        Action<EnquiryInformationEvent> action = ActionFactory.getAction(ActionTypeEnum.ENQUIRY_INFORMATION);
        EnquiryInformationEvent enquiryInformationEvent = new EnquiryInformationEvent();
        enquiryInformationEvent.setTransitOperation(cd144CType.getTransitOperation());
        enquiryInformationEvent.setCustomsOfficeOfDeparture(cd144CType.getCustomsOfficeOfDeparture());
        enquiryInformationEvent.setCompetentCustomsOfficeAtDeparture(cd144CType.getCompetentCustomsOfficeAtDeparture());
        enquiryInformationEvent.setCompetentCustomsOfficeAtDestination(cd144CType.getCompetentCustomsOfficeAtDestination());
        enquiryInformationEvent.setEnquiry(cd144CType.getEnquiry());
        enquiryInformationEvent.getResponseInformation().addAll(cd144CType.getResponseInformation());

        action.setPayload(enquiryInformationEvent);

        Movement movement = Movement.builder()
                                    .id(TEST_MOVEMENT_ID)
                                    .officeRoleType(OfficeRoleTypesEnum.DEPARTURE.getValue())
                                    .build();

        // when
        Message message = actionService.setUpActionMessage(action);
        when(dossierDataService.persistMessage(any())).thenReturn(message);
        when(dossierDataService.getMovementById(any(), anyBoolean())).thenReturn(movement);

        //verify
        assertDoesNotThrow(() -> actionService.completeAction(action));
        verify(dossierDataService, times(1)).saveEnquiryAdditionalInformation(any());
        verify(ccnMessageProducer, times(1)).publish(any(), any(), any());
    }

    @Test
    void testCompleteActionEnquiryInformationRequest() throws JAXBException, JsonProcessingException {

        // given
        String messageXml = xmlToString("CD145C.xml");
        CD145CType cd145CType = XmlConverter.buildMessageFromXML(messageXml, CD145CType.class);

        Action<EnquiryInformationRequestEvent> action = ActionFactory.getAction(ActionTypeEnum.ENQUIRY_INFORMATION_REQUEST);
        EnquiryInformationRequestEvent enquiryInformationRequestEvent = new EnquiryInformationRequestEvent();
        enquiryInformationRequestEvent.setTransitOperation(cd145CType.getTransitOperation());
        enquiryInformationRequestEvent.setCompetentCustomsOfficeAtDeparture(cd145CType.getCompetentCustomsOfficeAtDeparture());
        enquiryInformationRequestEvent.setCompetentCustomsOfficeAtDestination(cd145CType.getCompetentCustomsOfficeAtDestination());
        enquiryInformationRequestEvent.setEnquiry(cd145CType.getEnquiry());
        action.setPayload(enquiryInformationRequestEvent);

        // when
        Message message = actionService.setUpActionMessage(action);
        when(dossierDataService.persistMessage(any())).thenReturn(message);

        //verify
        assertDoesNotThrow(() -> actionService.completeAction(action));
        verify(dossierDataService, times(1)).saveEnquiryAdditionalInformation(any());
        verify(ccnMessageProducer, times(1)).publish(any(), any(), any());
    }

    private Optional<Movement> createMovement(OfficeRoleTypesEnum officeRoleType, ProcessingStatusTypeEnum processingStatus) {
        return Optional.ofNullable(processingStatus != null
                ? Movement.builder()
                          .id(TEST_MOVEMENT_ID)
                          .officeRoleType(officeRoleType.getValue())
                          .processingStatus(processingStatus != null
                                  ? processingStatus.getItemCode()
                                  : null)
                          .officeId(OFFICE_ID)
                          .build()
                : null);
    }

    private static Stream<Arguments> getActionFilteringCases() {
        // Argument List

        // OfficeRoleTypesEnum officeRole, List<String> userRoles, ProcessingStatusTypeEnum movementStatus,
        // List<WorkTask> openWorkTasks, List<Action> expectedAvailableGlobalActions, List<Action> expectedAvailableMovementActions

        WorkTask controlDecisionAfterUnloadingTask = newWorkTask("controlDecisionTask1",
                OfficeOfDestinationWorkTaskTypeEnum.CONTROL_DECISION_AFTER_UNLOADING_TASK.getCode());

        List<Action> expectedGlobalActions = Arrays.asList(
                newAction(ActionTypeEnum.MANUAL_ARRIVAL),
                newAction(ActionTypeEnum.REGISTER_NOTIFICATION_CROSSING_FRONTIER),
                newAction(ActionTypeEnum.REGISTER_NOTIFICATION_LEAVING_SECURITY_AREA),
                newAction(ActionTypeEnum.INCIDENT_NOTIFICATION),
                newAction(ActionTypeEnum.INCIDENT_NOTIFICATION_DEP),
                newAction(ActionTypeEnum.INCIDENT_NOTIFICATION_DEST),
                newAction(ActionTypeEnum.INCIDENT_NOTIFICATION_TRS),
                newAction(ActionTypeEnum.INCIDENT_NOTIFICATION_TRXS),
                newAction(ActionTypeEnum.MOVEMENT_QUERY_INCIDENT),
                newAction(ActionTypeEnum.ENQUIRY_INFORMATION),
                newAction(ActionTypeEnum.ENQUIRY_INFORMATION_REQUEST),
                newAction(ActionTypeEnum.MOVEMENT_QUERY_GLOBAL),
                newAction(ActionTypeEnum.ATR_REQUEST_UNKNOWN_MRN),
                newAction(ActionTypeEnum.AXR_REQUEST_UNKNOWN_MRN),
                newAction(ActionTypeEnum.RECOVERY_REQUEST_DEST),
                newAction(ActionTypeEnum.CD_MESSAGE_MANAGEMENT),
                newAction(ActionTypeEnum.OFFICE_ENQUIRY_RESPONSE));

        return Stream.of(
                Arguments.of(
                        null, null, Arrays.asList(), null, Arrays.asList(),
                        expectedGlobalActions,
                        Arrays.asList()
                ),
                Arguments.of(
                        OfficeRoleTypesEnum.DESTINATION, null, Arrays.asList(), null, Arrays.asList(),
                        expectedGlobalActions,
                        Arrays.asList()
                ),
                Arguments.of(
                        OfficeRoleTypesEnum.DESTINATION, null, Arrays.asList("admin"), null, Arrays.asList(),
                        expectedGlobalActions,
                        Arrays.asList()
                ),
                Arguments.of(
                        OfficeRoleTypesEnum.DESTINATION, null, Arrays.asList("admin"), ProcessingStatusTypeEnum.OODEST_AAR_CREATED, Arrays.asList(),
                        expectedGlobalActions,
                        Arrays.asList(
                                newAction(ActionTypeEnum.MANUAL_ARRIVAL_FOR_MOVEMENT),
                                newAction(ActionTypeEnum.REGISTER_INCIDENT),
                                newAction(ActionTypeEnum.RECOVERY_REQUEST_DEST_FOR_MOVEMENT),
                                newAction(ActionTypeEnum.SEALS_ACKNOWLEDGEMENT)
                        )
                ),
                Arguments.of(
                        OfficeRoleTypesEnum.DEPARTURE, null, Arrays.asList(), null, Arrays.asList(),
                        expectedGlobalActions,
                        Arrays.asList()
                ),
                Arguments.of(
                        OfficeRoleTypesEnum.DEPARTURE, null, Arrays.asList("admin"), null, Arrays.asList(),
                        expectedGlobalActions,
                        Arrays.asList()
                ),
                Arguments.of(
                        OfficeRoleTypesEnum.DEPARTURE, null, Arrays.asList("admin"), ProcessingStatusTypeEnum.OODEP_ACCEPTED, Arrays.asList(),
                        expectedGlobalActions,
                        Arrays.asList(
                                newAction(ActionTypeEnum.GUARANTEE_INTEGRITY_CHECK),
                                newAction(ActionTypeEnum.AMEND),
                                newAction(ActionTypeEnum.CANCELLATION_ON_DEMAND)
                        )
                ),
                Arguments.of(
                        OfficeRoleTypesEnum.EXIT, null, Arrays.asList(), null, Arrays.asList(),
                        expectedGlobalActions,
                        Arrays.asList()
                ),
                Arguments.of(
                        OfficeRoleTypesEnum.EXIT, null, Arrays.asList("admin"), null, Arrays.asList(),
                        expectedGlobalActions,
                        Arrays.asList()
                ),
                Arguments.of(
                        OfficeRoleTypesEnum.EXIT, null, Arrays.asList("admin"), ProcessingStatusTypeEnum.OOTRXS_AXR_CREATED, Arrays.asList(),
                        expectedGlobalActions,
                        Arrays.asList(
                                newAction(ActionTypeEnum.REGISTER_INCIDENT),
                                newAction(ActionTypeEnum.RECOVERY_REQUEST_TRXS_FOR_MOVEMENT)
                        )
                ),
                Arguments.of(
                        OfficeRoleTypesEnum.TRANSIT, null, Arrays.asList(), null, Arrays.asList(),
                        expectedGlobalActions,
                        Arrays.asList()
                ),
                Arguments.of(
                        OfficeRoleTypesEnum.TRANSIT, null, Arrays.asList("admin"), null, Arrays.asList(),
                        expectedGlobalActions,
                        Arrays.asList()
                ),
                Arguments.of(
                        OfficeRoleTypesEnum.TRANSIT, null, Arrays.asList("admin"), ProcessingStatusTypeEnum.OOTRS_ATR_CREATED, Arrays.asList(),
                        expectedGlobalActions,
                        Arrays.asList(
                                newAction(ActionTypeEnum.REGISTER_INCIDENT),
                                newAction(ActionTypeEnum.RECOVERY_REQUEST_TRS_FOR_MOVEMENT)
                        )
                ),
                Arguments.of(
                        OfficeRoleTypesEnum.DESTINATION, null, Arrays.asList("admin"), ProcessingStatusTypeEnum.OODEST_AAR_CREATED, Arrays.asList(
                                controlDecisionAfterUnloadingTask
                        ),
                        expectedGlobalActions,
                        Arrays.asList(
                                newAction(ActionTypeEnum.CONTROL_DECISION_AFTER_UNLOADING, controlDecisionAfterUnloadingTask, true),
                                newAction(ActionTypeEnum.MANUAL_ARRIVAL_FOR_MOVEMENT),
                                newAction(ActionTypeEnum.REGISTER_INCIDENT),
                                newAction(ActionTypeEnum.RECOVERY_REQUEST_DEST_FOR_MOVEMENT),
                                newAction(ActionTypeEnum.SEALS_ACKNOWLEDGEMENT)
                        )
                ),
                Arguments.of(
                        OfficeRoleTypesEnum.DESTINATION, null, Arrays.asList("admin"), ProcessingStatusTypeEnum.OODEST_AAR_REQUESTED, Arrays.asList(
                                controlDecisionAfterUnloadingTask
                        ),
                        expectedGlobalActions,
                        Arrays.asList(
                                newAction(ActionTypeEnum.CONTROL_DECISION_AFTER_UNLOADING, controlDecisionAfterUnloadingTask, true),
                                newAction(ActionTypeEnum.RECOVERY_REQUEST_DEST_FOR_MOVEMENT)
                        )
                ),
                Arguments.of(
                        OfficeRoleTypesEnum.DESTINATION, null, Arrays.asList(), null, Arrays.asList(),
                        expectedGlobalActions,
                        Arrays.asList()
                ),
                Arguments.of(
                        OfficeRoleTypesEnum.DEPARTURE, null, Arrays.asList(), ProcessingStatusTypeEnum.OODEP_ACCEPTED, Arrays.asList(),
                        expectedGlobalActions,
                        Arrays.asList(
                                newAction(ActionTypeEnum.GUARANTEE_INTEGRITY_CHECK),
                                newAction(ActionTypeEnum.AMEND),
                                newAction(ActionTypeEnum.CANCELLATION_ON_DEMAND)
                        )
                ),
                Arguments.of(
                        OfficeRoleTypesEnum.CARGO_DESTINATION, null, List.of(), ProcessingStatusTypeEnum.CARGO_OODEST_AAR_CREATED, List.of(),
                        expectedGlobalActions,
                        List.of(
                                newAction(ActionTypeEnum.CARGO_MANUAL_ARRIVAL_FOR_MOVEMENT)
                        )
                ),
                Arguments.of(
                        OfficeRoleTypesEnum.CARGO_DESTINATION, null, List.of(), ProcessingStatusTypeEnum.CARGO_OODEST_REJECTED, List.of(),
                        expectedGlobalActions,
                        List.of()
                )
        );
    }

    private static WorkTask newWorkTask(String workTaskId, String workTaskType) {
        return WorkTask.builder()
                       .workTaskId(workTaskId)
                       .type(workTaskType)
                       .build();
    }

    private static Action newAction(ActionTypeEnum actionType) {
        return newAction(actionType, null);
    }

    private static Action newAction(ActionTypeEnum actionType, WorkTask workTask) {
        return newAction(actionType, workTask, false);
    }

    private static Action newAction(ActionTypeEnum actionType, WorkTask workTask, boolean includeWorkTaskPropertiesMetadata) {
        Action action = ActionFactory.getAction(actionType);
        action.setWorkTask(workTask);
        if (includeWorkTaskPropertiesMetadata) {
            action.setMetadata(new HashMap<>());
            action.getMetadata().put("worktaskProperties", Optional.empty());
            action.setWorkTaskList(List.of(workTask));
        }
        return action;
    }

    protected Movement createMovement(String id, String officeRoleType, String officeId, String mrn, String lrn, String processingStatus) {
        return Movement.builder()
                       .id(id)
                       .officeRoleType(officeRoleType)
                       .officeId(officeId)
                       .mrn(mrn)
                       .lrn(lrn)
                       .processingStatus(processingStatus)
                       .build();
    }

    private static Action getAction(ActionTypeEnum actionTypeEnum, String movementId, CD180CType cd180CType) {
        Action action = ActionFactory.getAction(actionTypeEnum);
        if (ActionTypeEnum.INCIDENT_NOTIFICATION.equals(actionTypeEnum)) {
            IncidentNotificationEvent incidentNotificationEvent = new IncidentNotificationEvent();
            incidentNotificationEvent.setMovementId(movementId);
            incidentNotificationEvent.setTransitOperation(cd180CType.getTransitOperation());
            incidentNotificationEvent.setCustomsOfficeOfDeparture(cd180CType.getCustomsOfficeOfDeparture());
            incidentNotificationEvent.setCustomsOfficeOfIncidentRegistration(cd180CType.getCustomsOfficeOfIncidentRegistration());
            incidentNotificationEvent.setConsignment(cd180CType.getConsignment());
            action.setPayload(incidentNotificationEvent);
        } else if (ActionTypeEnum.INCIDENT_NOTIFICATION_DEP.equals(actionTypeEnum)) {
            IncidentNotificationDepEvent incidentNotificationEvent = new IncidentNotificationDepEvent();
            incidentNotificationEvent.setMovementId(movementId);
            incidentNotificationEvent.setTransitOperation(cd180CType.getTransitOperation());
            incidentNotificationEvent.setCustomsOfficeOfDeparture(cd180CType.getCustomsOfficeOfDeparture());
            incidentNotificationEvent.setCustomsOfficeOfIncidentRegistration(cd180CType.getCustomsOfficeOfIncidentRegistration());
            incidentNotificationEvent.setConsignment(cd180CType.getConsignment());
            action.setPayload(incidentNotificationEvent);
        } else if (ActionTypeEnum.INCIDENT_NOTIFICATION_DEST.equals(actionTypeEnum)) {
            IncidentNotificationDestEvent incidentNotificationEvent = new IncidentNotificationDestEvent();
            incidentNotificationEvent.setMovementId(movementId);
            incidentNotificationEvent.setTransitOperation(cd180CType.getTransitOperation());
            incidentNotificationEvent.setCustomsOfficeOfDeparture(cd180CType.getCustomsOfficeOfDeparture());
            incidentNotificationEvent.setCustomsOfficeOfIncidentRegistration(cd180CType.getCustomsOfficeOfIncidentRegistration());
            incidentNotificationEvent.setConsignment(cd180CType.getConsignment());
            action.setPayload(incidentNotificationEvent);
        } else if (ActionTypeEnum.INCIDENT_NOTIFICATION_TRXS.equals(actionTypeEnum)) {
            IncidentNotificationTrxsEvent incidentNotificationEvent = new IncidentNotificationTrxsEvent();
            incidentNotificationEvent.setMovementId(movementId);
            incidentNotificationEvent.setTransitOperation(cd180CType.getTransitOperation());
            incidentNotificationEvent.setCustomsOfficeOfDeparture(cd180CType.getCustomsOfficeOfDeparture());
            incidentNotificationEvent.setCustomsOfficeOfIncidentRegistration(cd180CType.getCustomsOfficeOfIncidentRegistration());
            incidentNotificationEvent.setConsignment(cd180CType.getConsignment());
            action.setPayload(incidentNotificationEvent);
        } else if (ActionTypeEnum.INCIDENT_NOTIFICATION_TRS.equals(actionTypeEnum)) {
            IncidentNotificationTrsEvent incidentNotificationEvent = new IncidentNotificationTrsEvent();
            incidentNotificationEvent.setMovementId(movementId);
            incidentNotificationEvent.setTransitOperation(cd180CType.getTransitOperation());
            incidentNotificationEvent.setCustomsOfficeOfDeparture(cd180CType.getCustomsOfficeOfDeparture());
            incidentNotificationEvent.setCustomsOfficeOfIncidentRegistration(cd180CType.getCustomsOfficeOfIncidentRegistration());
            incidentNotificationEvent.setConsignment(cd180CType.getConsignment());
            action.setPayload(incidentNotificationEvent);
        }

        return action;
    }

}
