////////////////////////////////////////////////////////////
//
// File: RuleEngineHandler.java
//
////////////////////////////////////////////////////////////
//
// Author: Sachin Jain
//
// Nokia - Confidential
// Do not use, distribute, or copy without consent of Nokia.
// Copyright (c) 2017, 2025 Nokia. All rights reserved.
//
////////////////////////////////////////////////////////////

package com.nokia.tpapps.ratingcore.engine.impl;

import com.alcatel.tpapps.common.common.TPAResources;
import com.alcatel.tpapps.common.common.context.ThreadLocalCommonContext;
import com.alcatel.tpapps.common.exception.OperationFailedException;
import com.alcatel.tpapps.common.par.GenericValue;
import com.alcatel.tpapps.common.par.GenericValueType;
import com.alcatel.tpapps.common.par.ModifiableEntityIf;
import com.alcatel.tpapps.common.utils.CommonUtil;
import com.alcatel.tpapps.common.utils.Pair;
import com.alcatel.tpapps.common.utils.TimeUtility;
import com.alcatel.tpapps.diameter.par.AnnouncementInformation;
import com.alcatel.tpapps.diameter.par.FinalUnitAction;
import com.alcatel.tpapps.diameter.par.RedirectAddressType;
import com.alcatel.tpapps.diameter.par.ReportingReason;
import com.alcatel.tpapps.diameter.par.TriggerType;
import com.alcatel.tpapps.policy.common.GenericValueWrapper;
import com.alcatel.tpapps.policy.common.plugin.GenericValueConverter;
import com.alcatel.tpapps.policy.common.plugin.RuleEnginePluginCache;
import com.alcatel.tpapps.policy.common.rule.GenericValueConversionUtility;
import com.alcatel.tpapps.policy.common.rule.RuleEngineConstants;
import com.alcatel.tpapps.policy.common.rule.TimeValueGenerator;
import com.nokia.tpapps.charging.ChargingPolicyConstants;
import com.nokia.tpapps.charging.common.rule.RuleSetType;
import com.nokia.tpapps.charging.common.rule.attribute.ChargingRuleAttributeName;
import com.nokia.tpapps.charging.server.rules.common.CustomDataActionUtils;
import com.nokia.tpapps.charging.server.rules.eval.ChargingPolicyDecisionResult;
import com.nokia.tpapps.charging.server.rules.eval.ChargingPolicyDecisionResultImpl;
import com.nokia.tpapps.charging.server.rules.eval.ChargingPolicyDecisionWorker;
import com.nokia.tpapps.common.services.BucketService;
import com.nokia.tpapps.common.services.CounterService;
import com.nokia.tpapps.common.services.EntityCounterInstanceService;
import com.nokia.tpapps.common.services.PolicyCounterInstanceService;
import com.nokia.tpapps.common.services.SubscriptionService;
import com.nokia.tpapps.common.services.ThresholdProfileGroupService;
import com.nokia.tpapps.common.services.enums.RatingCallActions;
import com.nokia.tpapps.common.services.enums.RuleEngineEnum;
import com.nokia.tpapps.common.services.notification.NotificationDetail;
import com.nokia.tpapps.common.services.notification.SpecificAssociatedEntities;
import com.nokia.tpapps.common.services.threshold.ThresholdProfileContextImpl;
import com.nokia.tpapps.common.services.threshold.ThresholdRuleEngineResult;
import com.nokia.tpapps.common.services.threshold.ThresholdRuleResultWrapper;
import com.nokia.tpapps.lifecycle.entitymodel.LifeCycleTrigger;
import com.nokia.tpapps.lifecycle.model.ExhaustEntityType;
import com.nokia.tpapps.lifecycle.model.LifeCycleType;
import com.nokia.tpapps.lifecycle.model.TriggerLifeCycleEntityType;
import com.nokia.tpapps.lifecycle.model.TriggerLifeCycleEntityTypeAsDevice;
import com.nokia.tpapps.notificationclient.AssociatedEntities;
import com.nokia.tpapps.notificationclient.MapBackedNotificationTemplateParameters;
import com.nokia.tpapps.notificationclient.NotificationCollection;
import com.nokia.tpapps.notificationclient.NotificationCollectionImpl;
import com.nokia.tpapps.rating.common.PostProcessingContextImpl;
import com.nokia.tpapps.rating.common.RatingConstants;
import com.nokia.tpapps.rating.error.RatingEngineResultCodes;
import com.nokia.tpapps.rating.factory.AutoInstallBundleHandler;
import com.nokia.tpapps.rating.factory.RatingEngineAnswer;
import com.nokia.tpapps.rating.factory.RatingEngineAnswerImpl;
import com.nokia.tpapps.rating.factory.RatingEngineRequest;
import com.nokia.tpapps.rating.factory.RatingEngineRequestImpl;
import com.nokia.tpapps.rating.factory.SyResultAnswer;
import com.nokia.tpapps.ratingcore.common.impl.AccountContextImpl;
import com.nokia.tpapps.ratingcore.common.impl.ApplicabilityState;
import com.nokia.tpapps.ratingcore.common.impl.ContextManagement;
import com.nokia.tpapps.ratingcore.common.impl.GroupContextImpl;
import com.nokia.tpapps.ratingcore.common.impl.GroupContextTransImpl;
import com.nokia.tpapps.ratingcore.common.impl.PassContext;
import com.nokia.tpapps.ratingcore.common.impl.REWrapper;
import com.nokia.tpapps.ratingcore.common.impl.RateContext;
import com.nokia.tpapps.ratingcore.common.impl.RateRetrievalContext;
import com.nokia.tpapps.ratingcore.common.impl.RuleEngineResult;
import com.nokia.tpapps.ratingcore.common.impl.RuleResultWrapper;
import com.nokia.tpapps.ratingcore.common.impl.SubscriptionContextImpl;
import com.nokia.tpapps.servicemodel.PolicyCounterUtility;
import com.nokia.tpapps.servicemodel.ServiceModelConstants;
import com.nokia.tpapps.servicemodel.SignalingStateType;
import com.nokia.tpapps.servicemodel.common.ThreadLocalCallContext;
import com.nokia.tpapps.servicemodel.context.SNRContext;
import com.nokia.tpapps.servicemodel.context.UserContextImpl;
import com.nokia.tpapps.servicemodel.lifecycle.utils.EDRUtility;
import com.nokia.tpapps.servicemodel.model.AVInstance;
import com.nokia.tpapps.servicemodel.model.AnnouncementDetail;
import com.nokia.tpapps.servicemodel.model.Bucket;
import com.nokia.tpapps.servicemodel.model.BucketInfo;
import com.nokia.tpapps.servicemodel.model.BucketInstance;
import com.nokia.tpapps.servicemodel.model.BucketStepOverride;
import com.nokia.tpapps.servicemodel.model.CallDiscount;
import com.nokia.tpapps.servicemodel.model.ChargingService;
import com.nokia.tpapps.servicemodel.model.ChargingServiceInstance;
import com.nokia.tpapps.servicemodel.model.ChargingServiceUsageType;
import com.nokia.tpapps.servicemodel.model.ConsumptionPriority;
import com.nokia.tpapps.servicemodel.model.Counter;
import com.nokia.tpapps.servicemodel.model.CounterInfo;
import com.nokia.tpapps.servicemodel.model.CounterInstanceClass;
import com.nokia.tpapps.servicemodel.model.CustomField;
import com.nokia.tpapps.servicemodel.model.EntityCounterInstance;
import com.nokia.tpapps.servicemodel.model.EntityCounterInstanceAttachMode;
import com.nokia.tpapps.servicemodel.model.EntityCounterInstanceDS;
import com.nokia.tpapps.servicemodel.model.EntityCounterInstanceId;
import com.nokia.tpapps.servicemodel.model.EntityInformation;
import com.nokia.tpapps.servicemodel.model.EntityTypeEnum;
import com.nokia.tpapps.servicemodel.model.KindOfUnit;
import com.nokia.tpapps.servicemodel.model.MSCCKey;
import com.nokia.tpapps.servicemodel.model.PolicyCounterInfo;
import com.nokia.tpapps.servicemodel.model.REUnitType;
import com.nokia.tpapps.servicemodel.model.Rate;
import com.nokia.tpapps.servicemodel.model.SignalingState;
import com.nokia.tpapps.servicemodel.model.Subscription;
import com.nokia.tpapps.servicemodel.model.SubscriptionChargingServiceDS;
import com.nokia.tpapps.servicemodel.model.Step;
import com.nokia.tpapps.servicemodel.model.ThresholdEntityType;
import com.nokia.tpapps.servicemodel.model.ThresholdInstance;
import com.nokia.tpapps.servicemodel.model.ThresholdProfile;
import com.nokia.tpapps.servicemodel.model.ThresholdProfileGroup;
import com.nokia.tpapps.servicemodel.model.ThresholdRuleAction;
import com.nokia.tpapps.servicemodel.model.ThresholdType;
import com.nokia.tpapps.servicemodel.model.Tier;
import com.nokia.tpapps.servicemodel.model.TieredRate;
import com.nokia.tpapps.servicemodel.model.TypeOfSubscription;
import com.nokia.tpapps.servicemodel.model.WholesaleInstance;
import com.nokia.tpapps.servicemodel.notificationclient.NotificationHandler;
import com.nokia.tpapps.servicemodel.par.CounterInstanceClassPar;
import com.nokia.tpapps.servicemodel.rating.ThresholdHandler;
import com.nokia.tpapps.servicemodel.service.BucketInstanceService;
import com.nokia.tpapps.servicemodel.service.ChargingServiceService;
import com.nokia.tpapps.servicemodel.service.RateService;
import com.nokia.tpapps.servicemodel.service.TieredRateService;
import com.nokia.tpapps.servicemodel.service.helper.SubscriptionServiceUtil;
import com.nokia.tpapps.servicemodel.service.impl.BucketInstanceServiceImpl;
import com.nokia.tpapps.servicemodel.service.impl.LoanRequestServiceImpl;
import com.nokia.tpapps.servicemodel.service.utils.PolicyCounterInstanceUtility;
import com.nokia.tpapps.servicemodel.util.CallDiscountUtility;
import com.nokia.tpapps.servicemodel.util.ThresholdDiscountUtility;
import com.nokia.tpapps.spccontrol.common.server.SPCConstants;
import com.nokia.tpapps.spscommon.beancontext.BeanContext;
import com.nokia.tpapps.spscommon.cdr.MVNOEDRProcessor;
import com.nokia.tpapps.spscommon.common.CSSelectionOrder;
import com.nokia.tpapps.spscommon.common.ClubbingUtility;
import com.nokia.tpapps.spscommon.common.GlobalConfig;
import com.nokia.tpapps.spscommon.common.NumberUtility;
import com.nokia.tpapps.spscommon.common.RatingMetricsAttributes;
import com.nokia.tpapps.spscommon.common.SNRType;
import com.nokia.tpapps.spscommon.common.SPSCommonConstants;
import com.nokia.tpapps.spscommon.common.globalconfigproperties.CLApplicabilityRuleOptimization;
import com.nokia.tpapps.spscommon.common.globalconfigproperties.LogDetailForErrorResultCode;
import com.nokia.tpapps.spscommon.common.globalconfigproperties.PolicyCounterNotificationRule;
import com.nokia.tpapps.spscommon.common.globalconfigproperties.VirtualPolicyCounter;
import com.nokia.tpapps.spscommon.common.globalconfigproperties.WholesaleEnabledValues;
import com.nokia.tpapps.spscommon.entitymodel.Services;
import com.nokia.tpapps.spscommon.entitymodel.srvdefcache.CacheThreadLocalVar;
import com.nokia.tpapps.spscommonentities.cdr.AbstractCustomCDR;
import com.nokia.tpapps.spscommonentities.cdr.BucketCDR;
import com.nokia.tpapps.spscommonentities.cdr.CDREngine;
import com.nokia.tpapps.spscommonentities.cdr.CopyCdrTagRuleElements;
import com.nokia.tpapps.spscommonentities.cdr.MainBalanceCDR;
import com.nokia.tpapps.spscommonentities.diameter.attribute.FinalUnitIndication;
import com.nokia.tpapps.spscommonentities.diameter.attribute.LoanOperation;
import com.nokia.tpapps.spscommonentities.diameter.attribute.MultipleServicesCreditControl;
import com.nokia.tpapps.spscommonentities.diameter.attribute.NCHFTrigger;
import com.nokia.tpapps.spscommonentities.diameter.attribute.RedirectServer;
import com.nokia.tpapps.spscommonentities.diameter.attribute.SSIDetail;
import com.nokia.tpapps.spscommonentities.diameter.attribute.Trigger;
import com.nokia.tpapps.spscommonentities.rule.resultcontext.CallDiscountEnum;
import com.nokia.tpapps.spscommonentities.rule.resultcontext.ChargingSelectionLogic;
import com.nokia.tpapps.spscommonentities.rule.resultcontext.NotificationDestinationType;
import com.nokia.tpapps.spscommonentities.rule.resultcontext.ResourceTriggerApplicable;
import com.nokia.tpapps.spscommonentities.rule.resultcontext.ResultContextType;
import com.nokia.tpapps.spscommonentities.rule.resultcontext.SelectRate;
import com.nokia.tpapps.spscommonentities.rule.resultcontext.TaxType;
import com.nokia.tpapps.spscommonentities.rule.resultcontext.ThresholdTriggerType;
import com.nokia.tpapps.spscommonentities.rule.sourcecontext.AccountContext;
import com.nokia.tpapps.spscommonentities.rule.sourcecontext.BundleContext;
import com.nokia.tpapps.spscommonentities.rule.sourcecontext.Call;
import com.nokia.tpapps.spscommonentities.rule.sourcecontext.CommonCallContext;
import com.nokia.tpapps.spscommonentities.rule.sourcecontext.DeviceContext;
import com.nokia.tpapps.spscommonentities.rule.sourcecontext.ECommerceCallContext;
import com.nokia.tpapps.spscommonentities.rule.sourcecontext.GroupContext;
import com.nokia.tpapps.spscommonentities.rule.sourcecontext.GyMessageContext;
import com.nokia.tpapps.spscommonentities.rule.sourcecontext.ImsServiceInformationContext;
import com.nokia.tpapps.spscommonentities.rule.sourcecontext.NchfChargingMessageContext;
import com.nokia.tpapps.spscommonentities.rule.sourcecontext.RatingPostProcessingContext;
import com.nokia.tpapps.spscommonentities.rule.sourcecontext.ResourcePostProcessingContext;
import com.nokia.tpapps.spscommonentities.rule.sourcecontext.ServiceInfoContext;
import com.nokia.tpapps.spscommonentities.rule.sourcecontext.SessionParameterContext;
import com.nokia.tpapps.spscommonentities.rule.sourcecontext.SourceContext;
import com.nokia.tpapps.spscommonentities.rule.sourcecontext.SourceContextType;
import com.nokia.tpapps.spscommonentities.rule.sourcecontext.SubscriptionContext;
import com.nokia.tpapps.spscommonentities.rule.sourcecontext.UserContext;
import com.nokia.tpapps.spscommonentities.rule.sourcecontext.types.AVPLevel;
import com.nokia.tpapps.spscommonentities.rule.sourcecontext.types.AggregationType;
import com.nokia.tpapps.spscommonentities.rule.sourcecontext.types.CallType;
import com.nokia.tpapps.spscommonentities.rule.sourcecontext.types.CustomFieldLevel;
import com.nokia.tpapps.spscommonentities.rule.sourcecontext.types.NotificationPolicy;
import com.nokia.tpapps.spscommonentities.rule.sourcecontext.types.NotificationSentControl;
import com.nokia.tpapps.spscommonentities.rule.sourcecontext.types.RequestType;
import com.nokia.tpapps.spscommonentities.rule.sourcecontext.types.ResourceTriggerType;
import com.nokia.tpapps.spscommonentities.rule.sourcecontext.types.SaveValueOfResource;
import com.nokia.tpapps.spscommonentities.rule.sourcecontext.types.TAGAction;
import com.nokia.tpapps.spscommonentities.rule.sourcecontext.types.TAGLevel;
import com.nokia.tpapps.subscribermodel.error.AlignBundleException;
import com.nokia.tpapps.subscribermodel.error.EntityBarredException;
import com.nokia.tpapps.subscribermodel.error.EntityFinalException;
import com.nokia.tpapps.subscribermodel.model.Account;
import com.nokia.tpapps.subscribermodel.model.AccountBalance;
import com.nokia.tpapps.subscribermodel.model.AccountSecondaryBalance;
import com.nokia.tpapps.subscribermodel.model.Device;
import com.nokia.tpapps.subscribermodel.model.DeviceIdentity;
import com.nokia.tpapps.subscribermodel.model.Group;
import com.nokia.tpapps.subscribermodel.model.LoanRequest;
import com.nokia.tpapps.subscribermodel.model.PolicyCounterInstance;
import com.nokia.tpapps.subscribermodel.model.User;
import com.nokia.tpapps.subscribermodel.service.AccountService;
import com.nokia.tpapps.subscribermodel.service.DeviceService;
import com.nokia.tpapps.subscribermodel.service.GroupService;
import com.nokia.tpapps.subscribermodel.service.UserService;
import com.nokia.tpapps.util.HistoryUtility;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.util.Strings;
import org.jetbrains.annotations.Nullable;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;

import static com.alcatel.tpapps.common.utils.CommonUtil.isNotNullOrEmpty;
import static com.alcatel.tpapps.common.utils.CommonUtil.isNullOrEmpty;
import static com.nokia.tpapps.common.services.notification.util.NotificationUtil
        .getAssociatedEntitiesFromGVW;
import static com.nokia.tpapps.servicemodel.model.ConsumptionPriority.CARRY_OVER_BUCKET;
import static com.nokia.tpapps.spscommon.common.SPSCommonConstants.DEVICE_PROFILE_LOGGING_PREFIX;


/**
 * This Class is use to access rule engine. This class call rule Engine,
 * received its response and convert this response into format understand by
 * core rating module
 */
public class RuleEngineHandler
{
    /**
     * Using this string literal in Logs for Call forward
     */
    private static final String CALL_FORWARD = "Device Site :: Call To Be forwarded to Group site: ";

    /**
     * Logger initialization.
     */
    static final Logger logger = TPAResources.getLogger();

    /**
     * This will be the default tier name for Tiered rates which will be
     * used as key for TieredRateInfoMap
     */
    static final String DEFAULT_TIER = "DEFAULT_TIER";

    /**
     * SY Result Context of Threshold Rule
     */
    private static final String SY_RESULT_CONTEXT = "SY";

    /**
     * Used to match with group id when device subscription
     */
    private static final String DEVICE = "Device";

    /** Counter Rule Table Prefix */
    private static final String RULE_SUFFIX = "-RULE";

    /**
     * Main Balance Rate Target
     */
    static final String MAIN_BALANCE_TARGET = "Main Balance";

    /**
     * suffix for rate created from tiered-rate
     */
    private static final String PRIMARY = "_Primary";

    /**
     * suffix for rate created from tiered-next-rate
     */
    private static final String SECONDARY = "_Secondary";

    /**
     * underscore
     */
    private static final String UNDERSCORE = "_";

    /**
     * logger message
     */
    private static final String SKIP_TRIGGER_EVENT = "Skipping triggering lifecycle event as the TriggerEventName";

    /**
     * logger message
     */
    private static final String IS_NULL_EMPTY = "is null or empty";

    private static boolean isZeroRate;

    /**
     * EntityCounterInstanceService Class.
     */
    private static EntityCounterInstanceService entityCounterInstanceService = Services
            .lookup(EntityCounterInstanceService.class);

    /**
     * Private constructor for Rule Engine Handler class
     */
    private RuleEngineHandler()
    {

    }

    /**
     * This Method trigger rule engine to fetch rule engine output to be used to
     * calculate further rating logic to deduct
     *
     * @param aInContexts its the rating engine request
     * @param aInSubChargingServiceDS Charging Service DS
     * @param aInCsInst Charging Service Instance
     * @param aInPassNo Pass Number of Charging Service
     * @param aInRequestWrapper Rating request object recived from call control
     * @param aInObjectChargingServices list of all subscription charging services.
     * @param aInPassContexts list of pass context information of CS's processed so far.
     * @return lRuleEngineResultList of Rule Engine result for further actions.
     */
    public static RuleResultWrapper triggerRuleEngineForTariff(
            Map<String, SourceContext> aInContexts,
            SubscriptionChargingServiceDS aInSubChargingServiceDS,
            ChargingServiceInstance aInCsInst,
            int aInPassNo,
            REWrapper aInRequestWrapper,
            List<SubscriptionChargingServiceDS> aInObjectChargingServices,
            List<PassContext> aInPassContexts)
    {
        TPAResources.debug(logger, "triggerRuleEngineForTariff Start");
        ChargingService lChargingService = aInSubChargingServiceDS.getChargingServiceDef();
        RuleResultWrapper lReWrapper = new RuleResultWrapper();
        RuleEngineResult lRuleEngineResult = new RuleEngineResult();
        List<RuleEngineResult> lRuleEngineResults = new ArrayList<>();
        boolean lSetRuleResult = false;
        String lBucketName = null;

        String lTariffName = null;
        if (!CommonUtil.isNullOrEmpty(lChargingService.getPasses()))
        {
            lTariffName = lChargingService.getPasses().get(aInPassNo).getTariff();
        }
        if (lTariffName == null)
        {
            TPAResources.error(logger, "Tariff for Charging Service is NULL: BYPASS");
            lReWrapper.setDefaultAction(RuleEngineEnum.RULE_ENGINE_BYPASS);
            return lReWrapper;
        }

        TPAResources.debug(logger, "Tariff evaluate for Charging Service: ",
                lChargingService.getId());

        // Create the object where results will be stored
        ChargingPolicyDecisionResult lResult =
                new ChargingPolicyDecisionResultImpl(lChargingService);

        boolean lEstimateRequest =
                aInRequestWrapper.getRatingEngineRequest().getCall().isEstimateRequest();
        // Evaluate the rules
        ChargingPolicyDecisionWorker.evaluateRuleTable(
                "RuleTable:" + lTariffName, RuleSetType.TARIFF, aInContexts,
                lResult, lEstimateRequest);

        if( WholesaleEnabledValues.Disabled != GlobalConfig.getWholesaleEnabled()
                && aInSubChargingServiceDS.getChargingServiceDef().getUsageType()
                    == ChargingServiceUsageType.WHOLESALE )
        {
            handleWholesaleRatingContext(aInRequestWrapper, lResult);
        }

        handleAddCustomFieldFromResultContext(lResult,
                aInRequestWrapper.getRatingEngineRequest());
        // Store the evaluation result in RatingEngineAnswer for VSA processing
        RatingEngineAnswer lReAnswer = aInRequestWrapper
                .getRatingEngineRequest().getRatingEngineAnswer();
        boolean lAnnouncementForTariffExist =
                AnnouncementUtility.checkAndRemoveAnnouncementForTariff(lResult, lReAnswer);
        RuleEngineHandler.addPolicyDecisionResultToAnswer(
                lTariffName, lResult, lReAnswer);
        if(!lAnnouncementForTariffExist)
        {
            AnnouncementUtility.setTariffIntoAnnouncementDetail(lTariffName, lReAnswer);
        }
        // Execute custom Data Action
        GroupContext lGroupContext = (GroupContext)
                aInContexts.get(SourceContextType.SPR_GROUP);
        DeviceContext lDeviceContext = (DeviceContext)
                aInContexts.get(SourceContextType.SPR_DEVICE);
        CommonCallContext lCommonCallContext =
                (CommonCallContext) aInContexts.get(SourceContextType.CALL_COMMON);
        CustomDataActionUtils.checkAndExecuteCustomDataAction(lDeviceContext, lGroupContext, lResult, lCommonCallContext);

        SubscriptionContext lSubscriptionContext = (SubscriptionContext)
                aInContexts.get(SourceContextType.SUBSCRIPTION);
        if ((lSubscriptionContext != null) && (lSubscriptionContext.getSubscriptionId() != null))
        {
            Subscription lSubscription = Services.lookup(SubscriptionService.class)
                .read(lSubscriptionContext.getSubscriptionId());
            CustomDataActionUtils.checkAndExecuteCustomDataActionInSubscription(lSubscription, lResult);
        }

        // look for no charge result
        Boolean lNoCharge = lResult.getAttributeResult(ResultContextType.RATING,
                ChargingRuleAttributeName.NO_CHARGE);
        if (lNoCharge != null)
        {
            // No charge, nothing to do FREE CALL
            TPAResources.debug(logger, "Result for Tariff ", lNoCharge);
            lReWrapper.setDefaultAction(RuleEngineEnum.RULE_ENGINE_NO_CHARGE);
            lRuleEngineResult.setNoCharge(true);
            lSetRuleResult = true;
        }
        // look for Rate Reject result
        Boolean lRateReject = lResult.getAttributeResult(ResultContextType.RATING,
                ChargingRuleAttributeName.RATE_REJECT);

        if (lRateReject != null)
        {
            // RateReject, should be released for this MSCC
            TPAResources.debug(logger, "Result for Tariff ", "Rate-Reject");
            lReWrapper.setDefaultAction(RuleEngineEnum.RULE_ENGINE_REJECT);

            lRuleEngineResult.setRateReject(true);
            lSetRuleResult = true;
        }

        lSetRuleResult = readAndSetCallDiscount(aInSubChargingServiceDS, lRuleEngineResult, lSetRuleResult, lResult);

        // look for Trigger Bou Activation Rule
        ArrayList<GenericValueWrapper> lTriggerBouAcivationAttribute = lResult.getAttributeResult(ResultContextType.RATING,
                ChargingRuleAttributeName.TRIGGER_BOU_ACTIVATION);

        if (!CommonUtil.isNullOrEmpty(lTriggerBouAcivationAttribute))
        {
            ChargingSelectionLogic lChargingSelectionLogic = null;

            String lPreRatingBouBundleName = lTriggerBouAcivationAttribute.get(0).getName();

            GenericValueConverter lValueConverter = RuleEnginePluginCache.getInstance(
                    ChargingPolicyConstants.CHARGING_PDF_APPLICATION).getConverter();
            if (lValueConverter != null)
            {
                lChargingSelectionLogic = (ChargingSelectionLogic) lValueConverter.extractFromGeneric(
                        lTriggerBouAcivationAttribute.get(0).getValue(), false);
            }
            if(lPreRatingBouBundleName != null && lChargingSelectionLogic != null)
            {
                TPAResources.debug(logger, "Result for Tariff ", "Trigger-BOU-Activation");
                if(lChargingSelectionLogic==ChargingSelectionLogic.REJECT)
                {
                    lReWrapper.setDefaultAction(RuleEngineEnum.RULE_ENGINE_TRIGGER_BOU_REJECT);
                }
                else if(lChargingSelectionLogic==ChargingSelectionLogic.STOP)
                {
                    lReWrapper.setDefaultAction(RuleEngineEnum.RULE_ENGINE_TRIGGER_BOU_STOP);
                }
                else if(lChargingSelectionLogic==ChargingSelectionLogic.CONTINUE)
                {
                    lReWrapper.setDefaultAction(RuleEngineEnum.RULE_ENGINE_TRIGGER_BOU_CONTINUE);
                }
                lRuleEngineResult.setPreRatingBouBundleName(lPreRatingBouBundleName);
                lSetRuleResult = true;
            }
        }

        //Look for Tax Selection
        Pair<String, TaxType> lTaxAttribs = getTaxSelectionAttributesFromRuleResult(lResult);
        String lTaxId = lTaxAttribs.first;
        TaxType lTaxType = lTaxAttribs.second;
        if (lTaxId != null && !lTaxId.equals(ChargingRuleAttributeName.GET_TAX_ID_FROM_APPLIED_RATE.toString()))
        {
            TPAResources.debug(logger, "Fetched Tax id: ", lTaxId);
            lRuleEngineResult.setTaxEntityKey(lTaxId);
            if (TaxType.INCLUDED_TO_COST.equals(lTaxType))
            {
                    lRuleEngineResult.setTaxIncluded(true);
            }
        }

        // Look for the rate
        RateContext lRateContext = aInRequestWrapper.getContextManagement().getRateContext();
        List<GenericValueWrapper> lRateResult = lResult.getAttributeResult(
                ResultContextType.RATING, ChargingRuleAttributeName.RATE);
        List<GenericValueWrapper> lBucketRateResult = lResult.getAttributeResult(
                ResultContextType.RATING, ChargingRuleAttributeName.BUCKET_GRANULARITY_SELECTION);

        List<String> lDefaultTieredRateInfoList =
                setTieredRateResult(lResult, lRuleEngineResult, false);
        TPAResources.debug(logger,
                SPSCommonConstants.TIERED_RATE , " Default Tiered-Rate Info: ",
                lDefaultTieredRateInfoList);
        if (prepareRuleEngineResultFromRateTarrif(aInRequestWrapper,
                lChargingService,
                lReWrapper, lRuleEngineResult, lTaxId, lRateContext,
                lRateResult,
                lBucketRateResult, lDefaultTieredRateInfoList))
        {
            lSetRuleResult = true;
        }
        if (lReWrapper.getDefaultAction() ==
                RuleEngineEnum.RULE_ENGINE_BYPASS ||
                lReWrapper.getDefaultAction() ==
                        RuleEngineEnum.RULE_ENGINE_ERROR)
        {
            return lReWrapper;
        }

        lRateContext =
                aInRequestWrapper.getContextManagement().getRateContext();
        lBucketName = lRuleEngineResult.getBucketEntityKey();
        lRuleEngineResult.setBucketEntityKey(null);
        if (lTaxId != null)
        {
            lTaxId = lRuleEngineResult.getTaxEntityKey();
        }

        //Set the bucket step override if exist
        setBucketStepOverrideInContextManagement(aInRequestWrapper, lResult);
        // Look for the rate external cost
        if ( !lSetRuleResult )
        {
            lRateResult = lResult.getAttributeResult(
                    ResultContextType.RATING, ChargingRuleAttributeName.RATE_EXTERNAL_COST);
            if (lRateResult != null)
            {
                // do rate stuff
                GenericValueWrapper lRateAttribute = lRateResult.get(0);
                /**
                 * rate satisfied For now these are just strings, but in the future
                 * these could be of other types, in which case you would need to
                 * check the type of the parameters as well.
                 */
                String lRateValue = lRateAttribute.getName();
                Pair<String,BigDecimal> lPair = getAttributesFromRateExternalCost(lRateAttribute);
                String lTargetValue = lPair.first;
                BigDecimal lOverridingCost = lPair.second;

                TPAResources.debug(logger, "Result for Tariff : Rate-> ",
                        lRateValue, ", Target->", lTargetValue, ", Overriding Cost->", lOverridingCost);

                if (lRateContext == null)
                {
                    aInRequestWrapper.getContextManagement().setRateContext(
                            new RateContext(lRateValue, lTaxId, lOverridingCost));
                    TPAResources.debug(logger, "CostBucketEnhancement: Rate: ", lRateValue,
                            " and OverridingCost: ", lOverridingCost, " is added in Rate Context for Bundle",
                            " with CS id: ", lChargingService.getId());
                }
                // If result is not Main Balance it has to be a monetary Bucket
                /* In case of MVNO, rule will return "MVNO!?Main Balance", to
                handle exceptional case of Main Balance remove MVNO!? from
                main Balance string */
                String lTargetValueWithoutMVNODelimiter =
                        MVNOEDRProcessor.getBusinessIdWithoutMVNO(lTargetValue);
                if (!lTargetValueWithoutMVNODelimiter.equals(MAIN_BALANCE_TARGET))
                {
                    lBucketName = lTargetValue;
                }

                else
                {
                    //Set string "Main Balance" instead of "MVNO!?Main Balance"
                    // in rule result
                    lRuleEngineResult.setAccountEntityKey(
                            lTargetValueWithoutMVNODelimiter);
                }
                lRuleEngineResult.setRateEntityKey(lRateValue);

                if ( lOverridingCost != null )
                {
                    lRuleEngineResult.setExternalCostProvided(true);
                    lRuleEngineResult.setOverridingCost(lOverridingCost);
                }

                lSetRuleResult = true;
            }
        }

        // Look for the bucket
        String lBucketInst = null;
        // If BucketName is empty means its a non monetary Bucket/money allowance bucket
        if (lBucketName == null)
        {
            GenericValueWrapper lBktAttribute = lResult.getAttributeResult(
                    ResultContextType.RATING, ChargingRuleAttributeName.BUCKET_SELECTION);
            if (lBktAttribute != null)
            {
                lBucketName = lBktAttribute.getName();
                GenericValueConverter lValueConverter = RuleEnginePluginCache.getInstance(
                        ChargingPolicyConstants.CHARGING_PDF_APPLICATION).getConverter();
                SelectRate lSelectRate = null;
                if (lValueConverter != null)
                {
                    lSelectRate = (SelectRate) lValueConverter
                            .extractFromGeneric(lBktAttribute.getValue(), false);
                }
                TPAResources.debug(logger, "CostBucketEnhancement: Bucket Selection-> ",
                        lBucketName, ", SelectRate-> ", lSelectRate);
                boolean lRateExternalEnabled = lSelectRate != null &&
                        lSelectRate == SelectRate.EXTERNAL;

                Bucket lBucket = null;
                if (lBucketName != null)
                {
                    lBucket = Services.lookup(BucketService.class).readBucket(lBucketName);
                }
                //Set lRateExternalEnabled flag true only for Monetary bucket And that too when
                // the RSU type is not CC_MONEY for monetary bucket
                if (!lRateExternalEnabled && lBucket != null && lBucket.getUnitType()
                        .getKindOfUnit() == KindOfUnit.MONEY && !isRsuTypeMoney(aInRequestWrapper))
                {
                    TPAResources.debug(logger, "CostBucketEnhancement: Setting lRateExternalEnabled",
                            "flag true in case of Monetory Bucket");
                    lRateExternalEnabled = true;
                }
                RateRetrievalContext lRateRetrievalCtx = new RateRetrievalContext(
                        aInSubChargingServiceDS, aInObjectChargingServices, aInContexts,
                        aInPassContexts, aInPassNo);
                if (processBucketForRateAllowance(aInRequestWrapper, lReWrapper, lRuleEngineResult,
                        lRateExternalEnabled, lBucket, lRateRetrievalCtx))
                {
                    // Move to next cs for processing as this one has only money allowance
                    // configured with rateAllowanceOffer flag as false and RSU is non-monetary.
                    return lReWrapper;
                }
            }
        }
        if (lBucketName != null)
        {
            // do bucket stuff rate satisfied
            TPAResources.debug(logger, "Result from rule, Bucket Id: ",
                    lBucketName);
            lBucketInst = getBucketInstId(aInCsInst, lBucketName);
            if (lBucketInst == null)
            {
                TPAResources.debug(logger, "Bucket Instance for bucket Def: " ,
                        lBucketName , " not attached with any CS ");
                lReWrapper.setDefaultAction(RuleEngineEnum.RULE_ENGINE_ERROR);
                return lReWrapper;
            }
            lRuleEngineResult.setBucketEntityKey(lBucketInst);
            lSetRuleResult = true;
        }

        //check threshold rate which may replace the tariff rate
        setThresholdRateResult(lResult, lRuleEngineResult, true);
        //Parse Rule result for Next-Rate resource for Surcharge.
        setNextRateRuleEngineResult(aInCsInst, lReWrapper,
                lRuleEngineResult, lResult);
        if (lReWrapper.getDefaultAction() == RuleEngineEnum.RULE_ENGINE_ERROR)
        {
            return lReWrapper;
        }

        // Set Rate Rounding Factor
        setRateRoundingFactorResult(aInRequestWrapper, lRuleEngineResult, lResult);

        // TODO Code for Counter
        // Look for the Counter
        String lCounterInst = null;
        List<String> lCounters = lResult.getAttributeResult(
                ResultContextType.RATING,
                ChargingRuleAttributeName.COUNTER_SELECTION);
        int lCounterSize = 0;
        if (lCounters != null)
        {
            lCounterSize = lCounters.size();
        }
        if (lCounterSize != 0)
        {
            // do counter stuff rate satisfied
            TPAResources.debug(logger,
                    "Result for Tariff: Number of Counters = ",
                    lCounters.size());
            for (int lCounterVal = 0; lCounterVal < lCounterSize; lCounterVal++)
            {
                String lCounterName = lCounters.get(lCounterVal);
                for (CounterInfo lCounterInfo : aInCsInst.getCounterInfoList())
                {
                    if (lCounterInfo.getCtrDefId().equals(lCounterName))
                    {
                        TPAResources.debug(logger, "Counter name: ",
                                lCounterName, ", Counter Instance ID: ",
                                lCounterInfo.getCtrInsId());
                        lRuleEngineResult.addCounterEntityList(lCounterInfo.getCtrInsId());
                    }
                }
                TPAResources.debug(logger, "Result for Tariff ",
                        lCounterInst);
                lSetRuleResult = true;
            }
        }

        if (lSetRuleResult)
        {
            lRuleEngineResults.add(lRuleEngineResult);
            checkMultiBucketsAndUpdateREResults(lRuleEngineResults,
                    lBucketName);
            lReWrapper.getRuleEngineResultList().addAll(lRuleEngineResults);
        }
        if (lReWrapper.getResultListsize() == 0)
        {
            // no action was returned that satisfies the charging Service
            // BYPASS CS
            TPAResources.debug(logger, "No Result from Rule BYPASS CS");
            lReWrapper.setDefaultAction(RuleEngineEnum.RULE_ENGINE_BYPASS);
        }
        return lReWrapper;
    }

    /**
     * used to read and set CallDiscount
     * @param aInSubChargingServiceDS           SubscriptionChargingServiceDS
     * @param lRuleEngineResult           RuleEngineResult
     * @param lSetRuleResult           boolean
     * @param lResult           ChargingPolicyDecisionResult
     */
    private static boolean readAndSetCallDiscount(SubscriptionChargingServiceDS aInSubChargingServiceDS,
                                                  RuleEngineResult lRuleEngineResult, boolean lSetRuleResult, ChargingPolicyDecisionResult lResult)
    {
        GenericValueWrapper lCallDiscountAttribute =
                lResult.getAttributeResult(ResultContextType.RATING, ChargingRuleAttributeName.CALL_DISCOUNT);
        if(lCallDiscountAttribute!=null)
        {
            CallDiscount lCallDiscountObj = setCallDiscountFromRule(lCallDiscountAttribute);

            lCallDiscountObj.setEntityDefName(aInSubChargingServiceDS.getChargingServiceDef().getId());
            lCallDiscountObj.setEntityType(CallDiscountUtility.ENTITY_TYPE_CHARGING_SERVICE);

            TPAResources.debug(logger,"Call-Discount retrieved from tariff : ",lCallDiscountObj);
            lRuleEngineResult.setCallDiscount(lCallDiscountObj);
            lSetRuleResult = true;
        }
        return lSetRuleResult;
    }

    /**
     * used to set CallDiscount
     * @param lCallDiscountAttribute           GenericValueWrapper
     *
     */
    private static CallDiscount setCallDiscountFromRule(GenericValueWrapper lCallDiscountAttribute)
    {
        GenericValueConverter lValueConverter = RuleEnginePluginCache.getInstance(
                ChargingPolicyConstants.CHARGING_PDF_APPLICATION).getConverter();
        BigDecimal lDiscountPercentage = null;
        CallDiscountEnum lCallDiscount = null;
        if (lValueConverter != null)
        {
            if (lCallDiscountAttribute.getValues()[0] != null)
            {
                //values specified in rule are extracted as per respective types : integer as BigInteger & decimal as
                // BigDecimal
                lDiscountPercentage = new BigDecimal(String.valueOf(lValueConverter
                        .extractFromGeneric(lCallDiscountAttribute.getValues()[0], false)));
            }
            if (lCallDiscountAttribute.getValues()[1] != null)
            {
                lCallDiscount = (CallDiscountEnum) lValueConverter
                        .extractFromGeneric(lCallDiscountAttribute.getValues()[1], false);
            }
        }
        CallDiscount lCallDiscountObj = new CallDiscount();
        lCallDiscountObj.setDiscountPercentage(lDiscountPercentage);
        lCallDiscountObj.setDiscountType(lCallDiscount);
        return lCallDiscountObj;
    }

    /**
     * @param aInRequestWrapper            Rating request object recived from
     *                                     call control
     * @param aInChargingService           chargingService
     * @param aInReWrapper                 RuleResultWrapper
     * @param aInRuleEngineResult          RuleEngineResult to be populated with
     *                                     rule Result
     * @param aInTaxId                     TaxId
     * @param aInRateContext               RateContext with rate information
     * @param aInRateResult                Rating.Rate result
     * @param aInBucketRateResult          Rating.Bucket_Granularity_Selection
     *                                     result
     * @param aInDefaultTieredRateInfoList Default Rate Info List if Tiered
     *                                     Rate
     * @return true/false to indicate RuleEngineResult is populated or not
     */
    private static boolean prepareRuleEngineResultFromRateTarrif(
            REWrapper aInRequestWrapper, ChargingService aInChargingService,
            RuleResultWrapper aInReWrapper,
            RuleEngineResult aInRuleEngineResult, String aInTaxId,
            RateContext aInRateContext, List<GenericValueWrapper> aInRateResult,
            List<GenericValueWrapper> aInBucketRateResult,
            List<String> aInDefaultTieredRateInfoList)
    {
        boolean lIsTieredRateDefined =
                CommonUtil.isNotNullOrEmpty(aInDefaultTieredRateInfoList);
        if (aInRateResult != null || aInBucketRateResult != null ||
                lIsTieredRateDefined)
        {
            boolean lIsRateDefined = false;
            String lRateValue = null;
            GenericValueWrapper lRateAttribute = null;
            if (lIsTieredRateDefined)
            {
                lRateValue = aInDefaultTieredRateInfoList.get(0);
                TPAResources.debug(logger, SPSCommonConstants.TIERED_RATE ,
                                " Default Tiered Rate Defined: ",
                        lRateValue);
            }
            else
            {
                lIsRateDefined = aInRateResult != null;
                TPAResources
                        .debug(logger, "Rating.Rate Defined: ", lIsRateDefined);
                // do rate stuff
                lRateAttribute = lIsRateDefined ? aInRateResult.get(0) :
                        aInBucketRateResult.get(0);
                /**
                 * rate satisfied For now these are just strings, but in the future
                 * these could be of other types, in which case you would need to
                 * check the type of the parameters as well.
                 */
                lRateValue = lRateAttribute.getName();
                TPAResources.debug(logger, "Rate Defined: ", lRateValue);
            }

            if (CommonUtil.isNullOrEmpty(lRateValue))
            {
                TPAResources.debug(logger,
                        "CostBucketEnhancement Rate : lRateValue is null. ",
                        "Setting Rule Engine error in default action");
                aInReWrapper.setDefaultAction(RuleEngineEnum.RULE_ENGINE_ERROR);
                return false;
            }
            Rate lRate = Services.lookup(ChargingServiceService.class)
                    .readRateInBaseUnits(lRateValue, aInChargingService);
            if (lRate == null)
            {
                TPAResources.debug(logger, "Rate :", lRateValue,
                        " doesn't exist in DB. ",
                        "Setting Rule engine error in default action");
                aInReWrapper.setDefaultAction(RuleEngineEnum.RULE_ENGINE_ERROR);
                return false;
            }

            setIsZeroRate(lRate);


            // Only in case of Rating.Rate, we need to check if it has only Rate defined i.e.
            // without any resources. In that case, we need to save the Rate in context.
            if (lIsRateDefined || lIsTieredRateDefined)
            {
                //if the rate should be fetched from the rate entity we should make sure it exist
                //so instead of doing it up and repeat  the steps of checking the existence of rate
                //we do it here after it normally check it
                if (aInTaxId != null &&
                        aInTaxId.equals(ChargingRuleAttributeName.GET_TAX_ID_FROM_APPLIED_RATE.toString()))
                {
                    aInTaxId = lRate.getTaxId();
                    TPAResources.debug(logger, "Fetched Tax id: ", aInTaxId);
                    aInRuleEngineResult.setTaxEntityKey(aInTaxId);
                    aInRuleEngineResult.setTaxIncluded(lRate.isTaxIncluded());
                }
                aInRequestWrapper.getRatingEngineRequest().getCall()
                        .getCommonCallContext().setApplicableRateId(lRateValue);
                KindOfUnit lRateKindOfUnit =
                        lRate.getRateType().getKindOfUnit();
                // lRateAttribute.getValues()).allMatch is added to compensate for an extra unwanted
                // null value in rateOnly configuration. It can be removed later.
                boolean lRateOnlyAllowance;
                if (lIsTieredRateDefined)
                {
                    if (aInDefaultTieredRateInfoList.size() > 1 && StringUtils
                            .isNotEmpty(aInDefaultTieredRateInfoList.get(1)))
                    {
                        lRateOnlyAllowance = false;
                    }
                    else
                    {
                        lRateOnlyAllowance = true;
                    }
                }
                else
                {
                    lRateOnlyAllowance = isRateOnlyAllowance(lRateAttribute);
                }
                // If the current bundle is rate type bundle, save the rate in rateContext if
                // it doesn't pre-exist already. This rate is used by other rate/bucket selection
                // bundles for which SelectRate value is 'SelectRate.EXTERNAL'.
                if (aInRateContext == null)
                {
                    if (lIsTieredRateDefined)
                    {
                        aInRequestWrapper.getContextManagement()
                                .setTieredRateContext(
                                        new RateContext(lRateValue,
                                                lRateOnlyAllowance,
                                                aInTaxId));
                    }
                    else
                    {
                        aInRequestWrapper.getContextManagement().setRateContext(
                                new RateContext(lRateValue, lRateOnlyAllowance,
                                        aInTaxId));
                    }
                    aInRequestWrapper.getRatingEngineRequest().getCall()
                            .getCommonCallContext().setSelectedRate(lRateValue);
                    TPAResources.debug(logger, "CostBucketEnhancement: Rate: ",
                            lRateValue,
                            " KindOfUnit: ", lRateKindOfUnit,
                            " RateOnlyAllowance: ", lRateOnlyAllowance,
                            " TaxationId: ", aInTaxId,
                            " is added in Rate Context for Bundle with ",
                            "CS id: ", aInChargingService.getId());
                }
                else
                {
                    TPAResources.debug(logger, "CostBucketEnhancement: Rate: ",
                            aInRateContext.getRateId(),
                            " already exists in Rate Context. Not adding new rate: ",
                            lRateValue,
                            " in context for CS id: ",
                            aInChargingService.getId());
                }
                // Rate Only bundle is bypassed in CS processing. Move to next cs for processing
                // as this one has only rate configured.
                if (lRateOnlyAllowance)
                {
                    //Even in rate only allowance, we need to consider call discount, if they are configured.
                    if (aInRuleEngineResult.getCallDiscount() != null)
                    {
                        aInRequestWrapper.getContextManagement()
                                .getCallDiscounts().add(
                                aInRuleEngineResult.getCallDiscount());
                    }
                    TPAResources.debug(logger,
                            "CostBucketEnhancement: Tariff for Charging Service",
                            " returns Rate Only with no Bucket selection/Main Balance: hence CS : ",
                            aInChargingService.getId(), " is BYPASS");
                    aInReWrapper.setDefaultAction(
                            RuleEngineEnum.RULE_ENGINE_BYPASS);
                    return false;
                }
            }

            String lTargetValue = null;
            SelectRate lSelectRate = null;
            if (lIsTieredRateDefined)
            {
                lTargetValue = aInDefaultTieredRateInfoList.get(1);
            }
            else
            {
                if (lRateAttribute.getValue() != null)
                {
                    lTargetValue = lRateAttribute.getValue().getValue();
                }
                else
                {
                    GenericValueConverter lValueConverter =
                            RuleEnginePluginCache.getInstance(
                                    ChargingPolicyConstants.CHARGING_PDF_APPLICATION)
                                    .getConverter();
                    if (lValueConverter != null)
                    {
                        lTargetValue =
                                (String) lValueConverter.extractFromGeneric(
                                        lRateAttribute.getValues()[0], false);
                        lSelectRate =
                                (SelectRate) lValueConverter.extractFromGeneric(
                                        lRateAttribute.getValues()[1], false);
                    }
                }
            }
            boolean lRateExternalEnabled = lSelectRate == SelectRate.EXTERNAL;
            TPAResources
                    .debug(logger, "Result for Tariff : Rate-> ", lRateValue,
                            ", Target->", lTargetValue, ", SelectRate->",
                            lSelectRate);
            // If result is not Main Balance it has to be a monetary Bucket
            /* In case of MVNO, rule will return "MVNO!?Main Balance", to handle
            exceptional case of Main Balance remove MVNO!? from main Balance
            string */
            String lTargetValueWithoutMVNODelimiter =
                    MVNOEDRProcessor.getBusinessIdWithoutMVNO(lTargetValue);
            if (!lTargetValueWithoutMVNODelimiter.equals(MAIN_BALANCE_TARGET))

            {
                aInRuleEngineResult.setBucketEntityKey(lTargetValue);
            }
            else
            {
                //Set string "Main Balance" instead of "MVNO!?Main Balance" in
                // rule result
                aInRuleEngineResult
                        .setAccountEntityKey(lTargetValueWithoutMVNODelimiter);
            }

            if (lRateExternalEnabled && aInRateContext != null &&
                    aInRateContext.getRateId() != null)
            {
                fillRuleEngineResultWithRateContext(aInRateContext,
                        aInRuleEngineResult);
            }
            else
            {
                // Taking rate from current tariff and set in rule engine result.
                aInRuleEngineResult.setRateEntityKey(lRateValue);
                if(lIsTieredRateDefined)
                {
                    TPAResources.debug(logger, SPSCommonConstants.TIERED_RATE ,
                            " Old Tiered Rate Defined:", lRateValue);
                    aInRuleEngineResult.setTieredRateOld(lRateValue);
                }
                TPAResources.debug(logger, "CostBucketEnhancement: Rate: ",
                        lRateValue,
                        " from rate tariff is set in Rule engine result.");
            }

            // If Rate.Bucket-Granularity-Selection is present instead of Rating.Rate then
            // set the flag to ignore cost/quantity in rate calculation
            if (!lIsRateDefined && !lIsTieredRateDefined)
            {
                TPAResources.debug(logger,
                        "Setting flag in rule engine result to ignore cost/quantity");
                aInRuleEngineResult.setIgnoreCostInRate(true);
            }
            return true;
        }
        return false;
    }

    private static void setIsZeroRate(Rate aInRate) {
        for (Step lStep : aInRate.getChargingSteps())
        {
            if (lStep.getCost() != null &&
                    (BigDecimal.ZERO).compareTo(lStep.getCost()) != 0)
            {
                isZeroRate = false;
                break;
            }
            isZeroRate = true;
        }
    }

    /**
     * This method used to extract the WholesaleRate and WholesaleNextRate
     * from RATING trigger in tariff evaluation, and set the values to
     * RatingEngineRequest
     * @param aInRequestWrapper REWrapper object having RatingEngineRequest
     *                          to set the values
     * @param aInResult ChargingPolicyDecisionResult object have
     *                  Result of tariff evaluation
     */
    private static void handleWholesaleRatingContext(REWrapper aInRequestWrapper
            , ChargingPolicyDecisionResult aInResult)
    {
        TPAResources.debug(logger,
                "Wholesale Enabled and set to Always for Session : "
                        , aInRequestWrapper.getContextManagement()
                        .getSessionId());
        List<GenericValueWrapper> lWholesaleRate =
                aInResult.getAttributeResult(ResultContextType.RATING,
                        ChargingRuleAttributeName.WHOLESALE_RATE);
        List<GenericValueWrapper> lWholesaleNextRate =
                aInResult.getAttributeResult(ResultContextType.RATING,
                        ChargingRuleAttributeName.WHOLESALE_NEXT_RATE);
        Pair<String, TaxType> lTaxAttribs = getTaxSelectionAttributesFromRuleResult(aInResult);
        String lWholesaleTax = lTaxAttribs.first;
        if (lWholesaleTax != null &&
                ChargingRuleAttributeName.GET_TAX_ID_FROM_APPLIED_RATE.toString().equals(lWholesaleTax))
        {
            Rate lRate = Services.lookup(RateService.class)
                    .readRateInBaseUnits(lWholesaleRate.get(0).getName());
            lWholesaleTax = lRate.getTaxId();
        }
        if (CommonUtil.isNotNullOrEmpty(lWholesaleRate) &&
                lWholesaleRate.get(0) != null)
        {
            if (CommonUtil.isNotNullOrEmpty(lWholesaleRate.get(0).getName()))
            {
                String lWholesaleValue = lWholesaleRate.get(0).getName();
                populateWholesaleRateToRE(aInRequestWrapper, lWholesaleValue);
                handleWholesaleNextRatingContext(aInRequestWrapper,
                        lWholesaleNextRate);
            }
            else if (lWholesaleRate.get(0).getValue() != null &&
                    lWholesaleRate.get(0).getValue().getType() ==
                            GenericValueType.STRING && CommonUtil
                    .isNotNullOrEmpty(lWholesaleRate.get(0)
                            .getValue().getValue()))
            {
                String lWholesaleValue = lWholesaleRate.get(0)
                        .getValue().getValue();
                populateWholesaleRateToRE(aInRequestWrapper, lWholesaleValue);
                handleWholesaleNextRatingContext(aInRequestWrapper,
                        lWholesaleNextRate);
            }
        }
        if (CommonUtil.isNotNullOrEmpty(lWholesaleTax))
        {
            aInRequestWrapper.getRatingEngineRequest().setWholesaleTax(
                    lWholesaleTax);
            TPAResources.debug(logger, "Setting Wholesale Tax : " ,
                    lWholesaleTax ,
                    " in Rating engine Request for Session : "
                    , aInRequestWrapper.getContextManagement()
                    .getSessionId());
        }
    }

    private static void populateWholesaleRateToRE(REWrapper aInRequestWrapper,
            String aInWholesaleValue)
    {
        aInRequestWrapper.getRatingEngineRequest()
                .setWholesaleCLExists(true);
        aInRequestWrapper.getRatingEngineRequest()
                .setWholesaleRate(aInWholesaleValue);
        TPAResources.debug(logger, "Setting Wholesale Rate: ",
                aInWholesaleValue,
                " in Rating engine Request for Session: "
                , aInRequestWrapper.getContextManagement()
                        .getSessionId());
    }

    private static void handleWholesaleNextRatingContext(
            REWrapper aInRequestWrapper,
            List<GenericValueWrapper> aInWholesaleNextRate)
    {
        if (CommonUtil.isNotNullOrEmpty(aInWholesaleNextRate) &&
                aInWholesaleNextRate.get(0) != null)
        {
            if (CommonUtil.isNotNullOrEmpty(
                    aInWholesaleNextRate.get(0).getName()))
            {
                String lWholesaleNextValue =
                        aInWholesaleNextRate.get(0).getName();
                populateWholesaleNextRateToRE(aInRequestWrapper,
                        lWholesaleNextValue);
            }
            else if (aInWholesaleNextRate.get(0).getValue() != null &&
                    aInWholesaleNextRate.get(0).getValue().getType() ==
                            GenericValueType.STRING && CommonUtil
                    .isNotNullOrEmpty(aInWholesaleNextRate.get(0)
                            .getValue().getValue()))
            {
                String lWholesaleNextValue = aInWholesaleNextRate.get(0)
                        .getValue().getValue();
                populateWholesaleNextRateToRE(aInRequestWrapper,
                        lWholesaleNextValue);
            }
        }
    }

    private static void populateWholesaleNextRateToRE(
            REWrapper aInRequestWrapper, String aInWholesaleNextValue)
    {
        aInRequestWrapper.getRatingEngineRequest()
                .setWholesaleNextRate(aInWholesaleNextValue);
        TPAResources.debug(logger,
                "Setting Wholesale Next Rate : " ,
                aInWholesaleNextValue,
                        " in Rating engine Request for Session : "
                        , aInRequestWrapper
                        .getContextManagement()
                        .getSessionId());
    }

    /**
     * This method sets Rate-RoundingFactor in Rule Engine Result. Rate-Rounding factor should be
     * considered only if it is configured with Rate, Next-Rate, Threshold-Rate and
     * Threshold-Next-Rate.
     *
     * @param aInRequestWrapper   {@link REWrapper} REWrapper object
     * @param aInRuleEngineResult {@link RuleEngineResult} Rule engine object returned by rule
     *                            engine.
     * @param aInResult           {@link ChargingPolicyDecisionResult} Charging Rule execution
     *                            result.
     */
    private static void setRateRoundingFactorResult(REWrapper aInRequestWrapper,
            RuleEngineResult aInRuleEngineResult, ChargingPolicyDecisionResult aInResult)
    {
        List<GenericValueWrapper> lRate = aInResult.getAttributeResult(
                ResultContextType.RATING, ChargingRuleAttributeName.RATE);
        GenericValueWrapper lNextRate = aInResult.getAttributeResult(
                ResultContextType.RATING, ChargingRuleAttributeName.NEXT_RATE);
        List<GenericValueWrapper> lThresholdRate = aInResult.getAttributeResult(
                ResultContextType.RATING, ChargingRuleAttributeName.THRESHOLD_RATE);
        List<GenericValueWrapper> lThresholdNextRate = aInResult.getAttributeResult(
                ResultContextType.RATING, ChargingRuleAttributeName.THRESHOLD_NEXT_RATE);

        // Consider rate rounding factor only with Rate, Next-Rate, Threshold-Rate and
        // Threshold-Next-Rate
        if (lRate != null || lNextRate != null || lThresholdRate != null ||
                lThresholdNextRate != null)
        {
            BigDecimal lRateRoundingFactor = aInResult.getAttributeResult(ResultContextType.RATING,
                    ChargingRuleAttributeName.RATE_ROUNDING_FACTOR);
            TPAResources.debug(logger, "Rate Rounding Factor from Tariff is: " ,
                    lRateRoundingFactor);
            // If Rate Rounding Factor is configured in Tariff, ignore its value from Global rules
            if (lRateRoundingFactor == null)
            {
                lRateRoundingFactor =
                        aInRequestWrapper.getRatingEngineRequest().getCall().getCommonCallContext()
                                .getRateRoundingFactor();
                TPAResources.debug(logger,
                        "Rate Rounding Factor from RSV(GY_CREDIT_REQUEST/GY_PRE_PROCESSING) ",
                        "is: ", lRateRoundingFactor);
            }

            // Do not set rate rounding factor
            // 1. if it is neither configured in Tariff nor in Global Rules or
            // 2. if it is configured as 0 or negative value or
            // 3. if it is less than minimum decimal value in Calculation Precision.
            // eg. Cal Precision = 5. Do not set Rate-RoundingFactor if it is less than 0.00001 or
            // 4. if it is less than minimum decimal value in DB Precision. eg. DB Precision = 2.
            // Do not set Rate-RoundingFactor if it is less than 0.01
            Double lDecimalValueWithCalcPrecision =
                    Math.pow(10, Math.negateExact(GlobalConfig.getCalcPrecision()));
            Double lDecimalValueWithDbPrecision =
                    Math.pow(10, Math.negateExact(GlobalConfig.getDbPrecision()));
            if (lRateRoundingFactor == null ||
                    lRateRoundingFactor.compareTo(BigDecimal.ZERO) <= 0 ||
                    lRateRoundingFactor.compareTo(new BigDecimal(lDecimalValueWithCalcPrecision)) <
                            0 ||
                    lRateRoundingFactor.compareTo(new BigDecimal(lDecimalValueWithDbPrecision)) <
                            0)
            {
                TPAResources.debug(logger, "Either Rate rounding factor is not " ,
                        "configured or it is negative or it is less than minimum decimal value " ,
                        "with Calculation Precision: " , lDecimalValueWithCalcPrecision , " or it " ,
                        "is less than minimum decimal value with DB Precision: "
                        , lDecimalValueWithDbPrecision , ". Hence, ignoring this factor." );
                return;
            }

            aInRuleEngineResult.setRateRoundingFactor(lRateRoundingFactor);
            if(null != aInRuleEngineResult.getNextRateRuleEngineResult())
            {
                aInRuleEngineResult.getNextRateRuleEngineResult()
                        .setRateRoundingFactor(lRateRoundingFactor);
            }
            TPAResources.debug(logger, "Rate Rounding Factor in rule engine result" ,
                    " is set to: " , lRateRoundingFactor);
        }
    }

    /**
     * Set in contextManagement if is not null the bucketStepOverride list
     * @param aInRequestWrapper - the REWrapper
     * @param lResult - the ChargingPolicyDecisionResult
     */
    private static void setBucketStepOverrideInContextManagement(REWrapper aInRequestWrapper, ChargingPolicyDecisionResult lResult)
    {
        // Steps override should be disabled at runtime for late calls.
        boolean lIsLateCall = Boolean.TRUE.equals(
                CacheThreadLocalVar.THREAD_LOCAL_VAR.getLateCall());
        if (!lIsLateCall && lResult != null)
        {
            List<GenericValueWrapper> lStepOverrideResult = lResult.getAttributeResult(
                    ResultContextType.RATING, ChargingRuleAttributeName.BUCKET_STEP_OVERRIDE);
            List<BucketStepOverride> bucketStepOverrideList = getBucketStepOverrideList(lStepOverrideResult);
            if (bucketStepOverrideList != null && !bucketStepOverrideList.isEmpty() && aInRequestWrapper != null && aInRequestWrapper.getContextManagement() != null)
            {
                aInRequestWrapper.getContextManagement().setBucketStepOverride(bucketStepOverrideList);
            }
        }

    }

    /**
     * Get the BucketStepOverride list
     * @param lStepOverrideResult - The GenericValueWrapper list
     * @return the BucketStepOverride list
     */
    private static List<BucketStepOverride> getBucketStepOverrideList(List<GenericValueWrapper> lStepOverrideResult)
    {
        List<BucketStepOverride> bucketStepOverrideList = new ArrayList<>();
        if (lStepOverrideResult != null)
        {
            for (GenericValueWrapper stepOverrideResult : lStepOverrideResult)
            {
                GenericValueConverter lConverter = RuleEnginePluginCache.getInstance(
                        ChargingPolicyConstants.CHARGING_PDF_APPLICATION).getConverter();
                String stepNumber = stepOverrideResult.getName();
                fillBucketStepOverrideList(bucketStepOverrideList, stepOverrideResult, lConverter, stepNumber);
            }
            return bucketStepOverrideList;

        }
        return bucketStepOverrideList;
    }

    /**
     * Fill the BucketStepOverride list with the step fee and step size
     * @param bucketStepOverrideList - the BucketStepOverride list
     * @param stepOverrideResult - the GenericValueWrapper
     * @param lConverter - the GenericValueConverter
     * @param stepNumber - the Step number
     */
    private static void fillBucketStepOverrideList(List<BucketStepOverride> bucketStepOverrideList, GenericValueWrapper stepOverrideResult,
                                                   GenericValueConverter lConverter, String stepNumber)
    {
        if (stepOverrideResult.getValues()[0] != null && stepOverrideResult.getValues()[1] != null)
        {
            Object stepFeeObject = lConverter.extractFromGeneric(
                    stepOverrideResult.getValues()[0], false);
            Object stepSizeObject = lConverter.extractFromGeneric(
                    stepOverrideResult.getValues()[1], false);
            BigDecimal stepSize = getGenericValueConverted(stepSizeObject);
            BigDecimal stepFee = getGenericValueConverted(stepFeeObject);
            bucketStepOverrideList.add(new BucketStepOverride(stepNumber, stepFee, stepSize));
        }
        else if (stepOverrideResult.getValues()[0] != null )
        {
            Object stepFeeObject = lConverter.extractFromGeneric(
                    stepOverrideResult.getValues()[0], false);
            BigDecimal stepFee = getGenericValueConverted(stepFeeObject);
            bucketStepOverrideList.add(new BucketStepOverride(stepNumber, stepFee, false));
        }
        else if (stepOverrideResult.getValues()[1] != null )
        {
            Object stepFeeObject = lConverter.extractFromGeneric(
                    stepOverrideResult.getValues()[1], false);
            BigDecimal stepSize = getGenericValueConverted(stepFeeObject);
            bucketStepOverrideList.add(new BucketStepOverride(stepNumber, stepSize, true));
        }
    }

    /**
     * Check if the Object is a BigInteger or BigDecimal
     * @param genericValue - the Object to be converted in BigDecimal
     * @return GenericValue Converted
     */
    private static BigDecimal getGenericValueConverted(Object genericValue)
    {
        BigDecimal stepNumber = null;
        if ( genericValue instanceof BigInteger)
        {
            stepNumber = new BigDecimal( (BigInteger) genericValue );
        }
        else if ( genericValue instanceof BigDecimal )
        {
            stepNumber = (BigDecimal)  genericValue;
        }
        return stepNumber;
    }

    /**
     * This method is to process the bucket returned from bucket-selection tariff for
     * rateAllowanceOffer bundles.
     * If rateAllowanceOffer is enabled, and rateContext is non-empty, then the bucket is
     * charged using the rate from the rateContext.
     * If rateAllowanceOffer is enabled, and rateContext is empty, then the bucket is charged
     * using the rate retrieved from other chargingServiceDSs.
     * If rateAllowanceOffer is disabled, we will bypass the CS for monetory buckets, For
     * Non-monetory buckets processing will be as existing.
     *
     * @param aInRequestWrapper the RatingEngineRequest object.
     * @param aInReWrapper the RuleResultWrapper to be populated with rule result.
     * @param aInRuleEngineResult the RuleEngineResult to be populated with rule Result
     * @param aInRateExternalEnabled flag to determine whether this CS/Bundle has rateAllowanceOffer
     * @param aInBucket bucket Object from Bucket-selection tariff
     * @param aInRateRetrievalContext contains all entities required o retrieve rate from CSs.
     * @return true if bucket is to be skipped for processing, else returns false.
     */
    private static boolean processBucketForRateAllowance(
            REWrapper aInRequestWrapper, RuleResultWrapper aInReWrapper,
            RuleEngineResult aInRuleEngineResult, boolean aInRateExternalEnabled,
            Bucket aInBucket, RateRetrievalContext aInRateRetrievalContext)
    {
        if (aInBucket != null)
        {
            if (aInRateExternalEnabled)
            {
                TPAResources.debug(logger, "CostBucketEnhancement: Processing bucket",
                        " selection: ", aInBucket.getId(), "for rateExternalOffer");
                RateContext lRateContext = aInRequestWrapper.getContextManagement()
                        .getRateContext();
                if (lRateContext != null && lRateContext.getRateId() != null)
                {
                    fillRuleEngineResultWithRateContext(lRateContext, aInRuleEngineResult);
                }
                else
                {
                    RateContext lRateContextFromCS = getRateCtxFromCsList(
                            aInRequestWrapper, aInRateRetrievalContext);
                    if (lRateContextFromCS != null)
                    {
                        fillRuleEngineResultWithRateContext(lRateContextFromCS, aInRuleEngineResult);
                        aInRequestWrapper.getContextManagement().setRateContext(lRateContextFromCS);
                        aInRequestWrapper.getRatingEngineRequest().getCall().getCommonCallContext()
                                .setSelectedRate(lRateContextFromCS.getRateId());
                    }
                    else
                    {
                        TPAResources.debug(logger, "CostBucketEnhancement: Rate not found",
                                " from remaining CS list for this allowance bucket: ",
                                aInBucket.getId());
                        aInReWrapper.setDefaultAction(RuleEngineEnum.RULE_ENGINE_ERROR);
                    }
                }
            }
            else
            {
                if (aInBucket.getUnitType().getKindOfUnit() == KindOfUnit.MONEY &&
                        !isRsuTypeMoney(aInRequestWrapper))
                {
                    // Move to next cs for processing as this one has only money allowance
                    // configured with rateAllowanceOffer flag as false and RSU is non-monetary.
                    TPAResources.debug(logger, "CostBucketEnhancement: Only Money bucket",
                            " configured in Bucket_selection with rateExternalOffer flag",
                            " disabled. hence CS is BYPASS");
                    aInReWrapper.setDefaultAction(RuleEngineEnum.RULE_ENGINE_BYPASS);
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Method to check if RSU type is CC-Money
     * @param aInRequestWrapper the Request wrapper object.
     * @return true if RSU type is CC-Money else return false
     */
    private static boolean isRsuTypeMoney(REWrapper aInRequestWrapper)
    {
        return aInRequestWrapper.getContextManagement().getRequestedServiceUnits().
                get(aInRequestWrapper.getContextManagement().getUnitIndex()).
                getUnitType().equalsIgnoreCase("MONEY");
    }

    /**
     * This method is used to get rate by evaluating the tariff of a particular pass
     * of input Charging service.
     *
     * @param aInRequestWrapper the Request wrapper object.
     * @param aInRateRetrievalCtx contains all entities required o retrieve rate from CSs.
     * @return the rate context if it rate configuration is found in any CS from the input CS list.
     */
    private static RateContext getRateCtxFromCsList(
            REWrapper aInRequestWrapper,
            RateRetrievalContext aInRateRetrievalCtx)
    {
        int lPassNo = aInRateRetrievalCtx.getPassNo();
        Map<String, SourceContext> lSrcContext = aInRateRetrievalCtx.getSourceContext();
        List<SubscriptionChargingServiceDS> lSubChrgServicesDSs =
                aInRateRetrievalCtx.getSubChrgServiceDSList();
        SubscriptionChargingServiceDS lCurrChrgServiceDS =
                aInRateRetrievalCtx.getCurrSubChrgServiceDS();
        TPAResources.debug(logger,"CostBucketEnhancement: Get Rate for CS: ",
                lCurrChrgServiceDS.getChargingServiceDef().getId(), " for pass no: ", lPassNo,
                " from remaining CS list.");
        PassHandler lCurrPassHandler = new PassHandler();
        lCurrPassHandler.getPassContextList().addAll(aInRateRetrievalCtx.getPassContexts());
        if (lPassNo == 0)
        {
            int lCurrDSIndex = lSubChrgServicesDSs.indexOf(lCurrChrgServiceDS);
            lCurrPassHandler.addToPassList(lCurrChrgServiceDS, lCurrDSIndex);
            for (int lIndex = lCurrDSIndex + 1; lIndex < lSubChrgServicesDSs.size(); lIndex++)
            {
                SubscriptionChargingServiceDS lSubChrgSrvDS = lSubChrgServicesDSs.get(lIndex);
                ChargingService lChrgSrvcDS = lSubChrgSrvDS.getChargingServiceDef();
                TPAResources.debug(logger,"CostBucketEnhancement: Get Rate in Pass 0: ",
                        "Processing CS: ", lChrgSrvcDS.getId());
                String lAppCond = lChrgSrvcDS.getAppCond();
                if (isApplicable(lAppCond, lSrcContext, lChrgSrvcDS))
                {
                    TPAResources.debug(logger,"CostBucketEnhancement: Charging Service : ",
                            lChrgSrvcDS.getId(), "is Applicable. Updating pass list.");
                    lCurrPassHandler.addToPassList(lSubChrgSrvDS, lIndex);
                    RateContext lOutRateCtx = getRateContext(aInRequestWrapper, lSrcContext,
                            lPassNo, lChrgSrvcDS);
                    if (lOutRateCtx != null)
                    {
                        return lOutRateCtx;
                    }
                }
            }
        }

        TPAResources.debug(logger,"CostBucketEnhancement: Get Rate: Processing",
                "pass context list. ");
        Collections.sort(lCurrPassHandler.getPassContextList(), new PassContext());
        for  (PassContext lPassCtx : lCurrPassHandler.getPassContextList())
        {
            if (lPassNo != 0 && lPassCtx.getPassId() < lPassNo)
            {
                TPAResources.debug(logger, "CostBucketEnhancement: This pass context with pass",
                        " id: ", lPassCtx.getPassId(), " is already processed. Moving to next.");
                continue;
            }
            RateContext lOutRateCtx = getRateContext(aInRequestWrapper, lSrcContext,
                    lPassCtx.getPassId(), lPassCtx.getChargingServiceDS().getChargingServiceDef());
            if (lOutRateCtx != null)
            {
                return lOutRateCtx;
            }
        }
        return null;
    }

    /**
     * This method is used to get rate by evaluating the tariff of a particular pass
     * of input Charging service.
     *
     * @param aInRequestWrapper the Request wrapper object.
     * @param aInContexts source context for rule evaluation
     * @param aInPassNo pass no of the CS tariff under process.
     * @param aInChrgSrvcDS charging service being processed.
     * @return the rate context if it rate config is found in pass no
     */
    private static RateContext getRateContext(
            REWrapper aInRequestWrapper, Map<String, SourceContext> aInContexts,
            int aInPassNo, ChargingService aInChrgSrvcDS)
    {
        if (aInChrgSrvcDS.getPasses().get(aInPassNo) == null)
        {
            TPAResources.debug(logger, "CostBucketEnhancement: ChargingServiceDS does ",
                    "not have pass info for pass no: ", aInPassNo);
            return null;
        }
        String lTariffName = null;
        if (!CommonUtil.isNullOrEmpty(aInChrgSrvcDS.getPasses()))
        {
            lTariffName = aInChrgSrvcDS.getPasses().get(aInPassNo).getTariff();
        }
        if (lTariffName == null)
        {
            TPAResources.debug(logger, "CostBucketEnhancement: Tariff for Charging Service",
                    " for pass no: ", aInPassNo, " is NULL");
            return null;
        }

        TPAResources.debug(logger, "CostBucketEnhancement: getRateContext : Tariff :",
                lTariffName, " evaluated for Charging Service: ", aInChrgSrvcDS.getId(),
                " and pass no: ", aInPassNo);

        ChargingPolicyDecisionResult lResult =
                new ChargingPolicyDecisionResultImpl(aInChrgSrvcDS);
        boolean lEstimateRequest =
                aInRequestWrapper.getRatingEngineRequest().getCall().isEstimateRequest();
        ChargingPolicyDecisionWorker.evaluateRuleTable(
                "RuleTable:" + lTariffName, RuleSetType.TARIFF, aInContexts,
                lResult, lEstimateRequest);

        //Look for Tax Selection
        Pair<String, TaxType> lTaxAttribs = getTaxSelectionAttributesFromRuleResult(lResult);
        String lTaxId = lTaxAttribs.first;
                List<GenericValueWrapper> lRateResult = lResult.getAttributeResult(
                ResultContextType.RATING, ChargingRuleAttributeName.RATE);
        if (lRateResult != null)
        {
            TPAResources.debug(logger, "CostBucketEnhancement:Rate Result found in",
                    " tariff evaluation. ");
            GenericValueWrapper lRateAttribute = lRateResult.get(0);
            String lRateValue = lRateAttribute.getName();
            Rate lRate = Services.lookup(RateService.class).readRateInBaseUnits(lRateValue);
            if (lTaxId != null && ChargingRuleAttributeName.GET_TAX_ID_FROM_APPLIED_RATE.toString().equals(lTaxId))
            {
                lTaxId= lRate.getTaxId();
            }
            if (lRate == null)
            {
                TPAResources.debug(logger, "CostBucketEnhancement: Rate :", lRateValue,
                        " doesn't exist in DB. ");
                return null;
            }
            KindOfUnit lRateKindOfUnit = lRate.getRateType().getKindOfUnit();
            boolean lRateOnlyAllowance = isRateOnlyAllowance(lRateAttribute);

            TPAResources.debug(logger, "CostBucketEnhancement: Rate: ", lRateValue,
                    " KindOfUnit: ", lRateKindOfUnit, " RateOnlyAllowance: ", lRateOnlyAllowance,
                    " TaxationId: ", lTaxId, " is added in Rate Context for Bundle with",
                    " CS id: ", aInChrgSrvcDS.getId());
            return new RateContext(lRateValue, lRateOnlyAllowance, lTaxId);
        }

        setBucketStepOverrideInContextManagement(aInRequestWrapper, lResult);
        // Look for the rate external cost
        lRateResult = lResult.getAttributeResult(
                    ResultContextType.RATING, ChargingRuleAttributeName.RATE_EXTERNAL_COST);
        if (lRateResult != null)
        {
            GenericValueWrapper lRateAttribute = lRateResult.get(0);
            /**
             * rate satisfied For now these are just strings, but in the future
             * these could be of other types, in which case you would need to
             * check the type of the parameters as well.
             */
             String lRateValue = lRateAttribute.getName();
             Pair<String,BigDecimal> lPair = getAttributesFromRateExternalCost(lRateAttribute);

            TPAResources.debug(logger, "CostBucketEnhancement: RateExternalCost:Result for Tariff : Rate-> ",
                    lRateValue, ", Target->", lPair.first, ", Overriding Cost->", lPair.second);
            return new RateContext(lRateValue, lTaxId, lPair.second);
        }
        return null;
    }

    /**
     * Method to get Target Value and Overriding cost from Rate External Cost.
     * @param aInRateAttribute the rate attributes for Rating.Rate function.
     * @return pair of Target Value and Overriding cost
     */
    private static Pair<String, BigDecimal> getAttributesFromRateExternalCost(
            GenericValueWrapper aInRateAttribute)
    {
        GenericValueConverter lConverter = RuleEnginePluginCache.getInstance(
                        ChargingPolicyConstants.CHARGING_PDF_APPLICATION).getConverter();
        String lTargetValue = (String) lConverter.extractFromGeneric(
                aInRateAttribute.getValues()[0], false);
        Object lOverridingCostObject = lConverter.extractFromGeneric(
                aInRateAttribute.getValues()[1], false);

        BigDecimal lOverridingCost = null;
        if ( lOverridingCostObject instanceof BigInteger )
        {
            lOverridingCost = new BigDecimal( (BigInteger) lOverridingCostObject );
        }
        else if ( lOverridingCostObject instanceof BigDecimal )
        {
            lOverridingCost = (BigDecimal)  lOverridingCostObject;
        }
        return new Pair<>(lTargetValue, lOverridingCost);
    }

    /**
     * This method checks if the specified configuration of rate is rate only.
     *
     * @param lRateAttribute the rate attributes for Rating.Rate function.
     * @return true if its a rate only tariff configuration.
     */
    private static boolean isRateOnlyAllowance(GenericValueWrapper lRateAttribute)
    {
        return lRateAttribute.getValue() == null &&
                (lRateAttribute.getValues() == null || Arrays.stream(
                        lRateAttribute.getValues()).allMatch(lVal -> lVal == null));
    }

    /**
     * Set the rule engine result for Next-Rate resource.
     *
     * @param aInCsInst                        {@link ChargingServiceInstance}
     *                                         instance.
     * @param aInReWrapper                     {@link RuleResultWrapper}
     *                                         instance.
     * @param aInRuleEngineResult              {@link RuleEngineResult} Rule
     *                                         engine object return by rule
     *                                         engine.
     * @param aInResult                        {@link ChargingPolicyDecisionResult}
     *                                         Charging Rule execution result.
     */
    private static void setNextRateRuleEngineResult(
            ChargingServiceInstance aInCsInst, RuleResultWrapper aInReWrapper,
            RuleEngineResult aInRuleEngineResult,
            ChargingPolicyDecisionResult aInResult)
    {
        GenericValueWrapper lNextRateValueWraper = aInResult.getAttributeResult(
                ResultContextType.RATING, ChargingRuleAttributeName.NEXT_RATE);
        GenericValueWrapper lNextBktValueWrapper = aInResult.getAttributeResult(
                ResultContextType.RATING,
                ChargingRuleAttributeName.NEXT_BUCKET_SELECTION);
        List<GenericValueWrapper> lNxtBktGranulrValWrapper = aInResult
                .getAttributeResult(ResultContextType.RATING,
                        ChargingRuleAttributeName.NEXT_BUCKET_GRANULARITY_SELECTION);
        //set Tiered Secondary Rate
        List<String> lDefaultTieredNextRateInfoList =
                setTieredRateResult(aInResult, aInRuleEngineResult, true);
        TPAResources.debug(logger, SPSCommonConstants.TIERED_RATE ,
                        " Default Tiered-Next-Rate Info: ",
                lDefaultTieredNextRateInfoList);
        boolean lIsTieredNextRateDefined =
                CommonUtil.isNotNullOrEmpty(lDefaultTieredNextRateInfoList);
        String lNextBucketInstance = null;
        RuleEngineResult lNextRateRuleEngineResult = null;
        if (lNextRateValueWraper != null || (lIsTieredNextRateDefined &&
                lDefaultTieredNextRateInfoList != null))
        {
            String lNextRateValue = null;
            String lNextRateTargetName = null;
            if (lIsTieredNextRateDefined &&
                    lDefaultTieredNextRateInfoList != null)
            {
                lNextRateRuleEngineResult =
                        aInRuleEngineResult.getNextRateRuleEngineResult();
                lNextRateValue = lDefaultTieredNextRateInfoList.get(0);
                TPAResources.debug(logger, SPSCommonConstants.TIERED_RATE ,
                        " Tiered Service Fee is defined:", lNextRateValue);
                if (lDefaultTieredNextRateInfoList.size() > 1)
                {
                    lNextRateTargetName =
                            lDefaultTieredNextRateInfoList.get(1);
                }
            }
            else
            {
                lNextRateRuleEngineResult = initialiseNxtRateRuleResltObj();
                lNextRateValue = lNextRateValueWraper.getName();
                lNextRateTargetName =
                        lNextRateValueWraper.getValue().getValue();
            }
            TPAResources.debug(logger, "Result for Tariff Next-Rate is Rate-> ",
                    lNextRateValue, ", Target->", lNextRateTargetName);
            // If result is not Main Balance it has to be a monetary Bucket
            /* In case of MVNO, rule will return "MVNO!?Main Balance",
            to handle exceptional case of Main Baance // remove MVNO!? from
            main Balance string */
            String lNextRateTargetNameWithoutMVNODelimiter = MVNOEDRProcessor
                    .getBusinessIdWithoutMVNO(lNextRateTargetName);
            if (!lNextRateTargetNameWithoutMVNODelimiter
                    .equals(MAIN_BALANCE_TARGET))
            {
                // do bucket stuff rate satisfied
                TPAResources.debug(logger,
                        "Result from rule,Monetary Bucket" , " Name: ",
                        lNextRateTargetName);
                lNextBucketInstance = getNxtBucketInstance(aInCsInst,
                        lNextRateTargetName, aInReWrapper);
                if (lNextBucketInstance == null)
                {
                    return;
                }
                BucketInstance lBucketInstance = Services
                        .lookup(BucketInstanceService.class)
                        .read(lNextBucketInstance);
                if (lBucketInstance == null
                        || (!lIsTieredNextRateDefined &&
                        !lBucketInstance.getUnitType().isMonetary()))
                {
                    TPAResources.debug(logger,
                            "Either Bucket for Next-Rate can not"
                                    ,
                                    " be a Non-Monetary bucket or bucketinstance is null.");
                    aInReWrapper.setDefaultAction(
                            RuleEngineEnum.RULE_ENGINE_ERROR);
                    return;
                }
                lNextRateRuleEngineResult
                        .setBucketEntityKey(lNextBucketInstance);
            }
            else
            {
                //Set string "Main Balance" instead of "MVNO!?Main Balance" in
                // rule result
                lNextRateRuleEngineResult
                        .setAccountEntityKey(
                                lNextRateTargetNameWithoutMVNODelimiter);
            }
            lNextRateRuleEngineResult.setRateEntityKey(lNextRateValue);
            if (lIsTieredNextRateDefined)
            {
                TPAResources.debug(logger, SPSCommonConstants.TIERED_RATE ,
                        " Tiered Next Rate Old Defined:", lNextRateValue);
                lNextRateRuleEngineResult.setTieredRateOld(lNextRateValue);
            }
        }
        else if (lNextBktValueWrapper != null)
        {
            lNextRateRuleEngineResult = initialiseNxtRateRuleResltObj();
            String lNextRateTargetName = lNextBktValueWrapper.getName();
            TPAResources.debug(logger,
                    "Result for Tariff Next-Bucket is Target->",
                    lNextRateTargetName);
            lNextBucketInstance = getNxtBucketInstance(aInCsInst,
                    lNextRateTargetName, aInReWrapper);
            if (lNextBucketInstance == null)
            {
                return;
            }
            else
            {
                lNextRateRuleEngineResult
                        .setBucketEntityKey(lNextBucketInstance);
            }
        }
        else if (lNxtBktGranulrValWrapper != null)
        {
            lNextRateRuleEngineResult = initialiseNxtRateRuleResltObj();
            String lNextRateValue = lNxtBktGranulrValWrapper.get(0).getName();
            String lNextRateTargetName =
                    lNxtBktGranulrValWrapper.get(0).getValue().getValue();
            lNextBucketInstance = getNxtBucketInstance(aInCsInst,
                    lNextRateTargetName, aInReWrapper);
            if (lNextBucketInstance == null)
            {
                return;
            }
            else
            {
                lNextRateRuleEngineResult
                        .setBucketEntityKey(lNextBucketInstance);
                lNextRateRuleEngineResult.setRateEntityKey(lNextRateValue);
                lNextRateRuleEngineResult.setIgnoreCostInRate(true);
            }
        }

        List<GenericValueWrapper> lThrNextRateValueWraper =
                aInResult.getAttributeResult(ResultContextType.RATING,
                        ChargingRuleAttributeName.THRESHOLD_NEXT_RATE);
        if (lThrNextRateValueWraper != null)
        {
            if (lNextRateRuleEngineResult == null)
            {
                lNextRateRuleEngineResult = new RuleEngineResult();
                lNextRateRuleEngineResult.setIsNextRateRuleEngineResult(true);

            }
            setThresholdRateResult(aInResult,lNextRateRuleEngineResult,false);
        }
        if (lNextRateRuleEngineResult != null)
        {
            lNextRateRuleEngineResult.setTaxEntityKey(aInRuleEngineResult.
                getTaxEntityKey());
            lNextRateRuleEngineResult.setTaxIncluded(aInRuleEngineResult.isTaxIncluded());
            aInRuleEngineResult
                .setNextRateRuleEngineResult(lNextRateRuleEngineResult);
        }
    }

    /**
     * Check the bucket-name got from rule is matched in bucket info list. If
     * matched then return matching bucket instanceId.
     *
     * @param aInCsInst     Charging Service Instance.
     * @param aInBucketName Bucket-Name gtom the tariff rule
     * @return the bucket instance id if the bucket name from tariff rule
     * matched in the list of bucket info.
     */
    public static String getBucketInstId(ChargingServiceInstance aInCsInst,
            String aInBucketName)
    {
        String aInBucketInstance = null;
        for (BucketInfo lBucketInfo : aInCsInst.getBucketInfoList())
        {
            if (lBucketInfo.getBktDefName().equals(aInBucketName))
            {
                aInBucketInstance = lBucketInfo.getBktInstId();
                TPAResources.debug(logger,
                        "Result from rule Bucket Instance Id :  ",
                        aInBucketInstance);
                return aInBucketInstance;
            }
        }
        return aInBucketInstance;
    }

    /**
     * This method will check for VSA and save it for CCA use
     *
     * @param aInRuleTableName Rule table name evaluated.
     * @param aInRuleEngineResult Rule Engine Result List that need to save.
     * @param aInOutReAnswer Rating answer object created by call control module
     */
    public static void addPolicyDecisionResultToAnswer(
            String aInRuleTableName,
            ChargingPolicyDecisionResult aInRuleEngineResult,
            RatingEngineAnswer aInOutReAnswer)
    {
        if (aInRuleEngineResult != null && aInOutReAnswer != null)
        {
            Map<String, Map<String, Object>> lResultMap =
                    aInRuleEngineResult.getResult();
            Map<String, Object> lVsaMapGy = lResultMap
                    .get(ResultContextType.GY);
            Map<String, Object> lVsaMapNchf = lResultMap
                    .get(ResultContextType.NCHF_CHARGING);

            List<Pair<String, ChargingPolicyDecisionResult>>
            lDecisionResultList = aInOutReAnswer.
                    getRulesetEvaluationResultList();
            // Only when result context "GY"/"NCHF_CHARGING" exists in
            // evaluation result, write back the evaluation result to
            // rating engine answer
            if (lVsaMapGy != null || lVsaMapNchf != null)
            {
                // Keep no duplicated result evaluated by same ruleset/ruletable
                for (Pair<String, ChargingPolicyDecisionResult>
                        lResultPair : lDecisionResultList)
                {
                    if (lResultPair != null &&
                            lResultPair.first.equals(aInRuleTableName))
                    {
                        // Found the same result of same ruletable/ruleset, so
                        // only replace the result value and return at once
                        lResultPair.second = aInRuleEngineResult;
                        AnnouncementUtility.updateAnnouncementDetails(aInRuleTableName, aInRuleEngineResult, aInOutReAnswer);
                        return;
                    }
                }

                // Else, not found its result, append it to keep the order
                lDecisionResultList.add(
                        new Pair<>(aInRuleTableName, aInRuleEngineResult));
                AnnouncementUtility.addAnnouncementDetails(aInRuleTableName, aInRuleEngineResult, aInOutReAnswer);
            }
            else if (aInRuleEngineResult.getAttributeResult(ResultContextType.CALL_RESULT,
                    ChargingRuleAttributeName.SERVICE_PARAMETER_INFO) != null)
            {
                TPAResources.debug(logger, "rule result include SPI action");
                lDecisionResultList.add(new Pair<>(aInRuleTableName, aInRuleEngineResult));
            }
        }
    }

    /**
     * This method will check for Multiple Buckets and create Rule Engine
     * Results for all and adds into passed Rule Engine Results List.
     *
     * @param aInRuleEngineResults Rule Engine Result List that need to update.
     * @param aInBucketDef Bucket definition name.
     */
    private static void checkMultiBucketsAndUpdateREResults(
            List<RuleEngineResult> aInRuleEngineResults, String aInBucketDef)
    {
        TPAResources.debug(logger, "RuleEngineHandler :: "
                , "checkMultiBucketsAndUpdateREResults : Start");
        if (aInBucketDef != null)
        {
            TPAResources.debug(logger, "RuleEngineHandler :: " ,
                    "checkMultiBucketsAndUpdateREResults : BucketDefinition: " ,
                    aInBucketDef , " found from Rule");
            String lBucketEntityKey = aInRuleEngineResults.get(0)
                    .getBucketEntityKey();
            checkMultiBucketsForREResults(aInRuleEngineResults,
                    lBucketEntityKey);
        }
    }

    /**
     * This method will check for Multiple Buckets and create Rule Engine
     * Results for all and adds into passed Rule Engine Results List.
     * @param aInRuleEngineResults Rule Engine Result List that need to update
     * @param aInBkInstId Bucket instance id
     */
    public static void checkMultiBucketsForREResults(
            List<RuleEngineResult> aInRuleEngineResults, String aInBkInstId)
    {
        BucketInstance lBucketInstance = Services
                .lookup(BucketInstanceServiceImpl.class)
                .readBucketInstanceInBaseUnits(aInBkInstId);
        if (null == lBucketInstance)
        {
            TPAResources.debug(logger, "RuleEngineHandler :: lBucketInstance is null for id: ",
                    aInBkInstId);
            return;
        }
        List<String> lBucketInstanceIds = lBucketInstance
                .getBktInstanceIDList();
        if ((lBucketInstanceIds != null) && (!lBucketInstanceIds.isEmpty()))
        {
            TPAResources.debug(logger, "RuleEngineHandler :: " ,
                    "checkMultiBucketsForREResults : carry over " ,
                    "buckets found for " , lBucketInstance , ". Creating " ,
                    "Rule engine results for carry over bucket instances");
            for(String lBktInstId : lBucketInstanceIds)
            {
                RuleEngineResult lRuleEngineResult = new RuleEngineResult(
                        aInRuleEngineResults.get(0));
                lRuleEngineResult.setBucketEntityKey(lBktInstId);
                aInRuleEngineResults.add(lRuleEngineResult);
            }
            sortRuleEngineResults(aInRuleEngineResults, lBucketInstance);
        }
    }

    /**
     * This method sorts passed Rule Engine Result List for Multiple Buckets.
     * For now this method do sorting for only two.
     * @param aInRuleEngineResults  Rule Engine Result List that needs to update.
     * @param aInZeroIndexBktInst   Bucket Instance Object of first index of
     *                              passed Rule Engine Result List.
     *
     */
    private static void sortRuleEngineResults(
            List<RuleEngineResult> aInRuleEngineResults,
            BucketInstance aInZeroIndexBktInst)
    {
        TPAResources.debug(logger,
                "RuleEngineHandler " , "sortRuleEngineResults: Start");
        int lmaxMultiBuckets = 2;
        boolean lIsLateCall = Boolean.TRUE.equals(
                CacheThreadLocalVar.THREAD_LOCAL_VAR.getLateCall());
        Long lEventTime = CacheThreadLocalVar.THREAD_LOCAL_VAR.getEventTime();
        if ((aInRuleEngineResults != null)
                && (aInRuleEngineResults.size() == lmaxMultiBuckets))
        {
            ConsumptionPriority lConsumptionPriority = null;
            /*
            if late call and second bucket is carry over bucket and the eventTime
            is in previous cycle then swap priorities to consume from carryOver
             */
            boolean lCarryOverLateRateMode = lIsLateCall && lEventTime
                    .compareTo(aInZeroIndexBktInst.getCycleEffectiveTime()) <
                    0 && !HistoryUtility.isCurrentBucketConsumption();
            if (lCarryOverLateRateMode)
            {
                lConsumptionPriority = CARRY_OVER_BUCKET;
            }
            else if (aInZeroIndexBktInst.getIsCarryOver())
            {
                BucketInstance lRegBucketInstance = Services.lookup(
                        BucketInstanceServiceImpl.class)
                        .readBucketInstanceInBaseUnits(
                                aInZeroIndexBktInst.getBktInstanceIDList()
                                        .get(0));
                lConsumptionPriority =
                        lRegBucketInstance.getConsumptionPriority();
            }
            else
            {
                lConsumptionPriority =
                        aInZeroIndexBktInst.getConsumptionPriority();
            }
            if (lConsumptionPriority != null)
            {
                Integer lRegularBucketIndex = null;
                switch (lConsumptionPriority)
                {
                    case REGULAR_BUCKET:
                        TPAResources.debug(logger, "RuleEngineHandler " ,
                                "sortRuleEngineResults: CarryOverBucket Scenario " ,
                                "with Regular Bucket as a Consumption Priority");
                        if (aInZeroIndexBktInst.getIsCarryOver())
                        {
                            Collections.swap(aInRuleEngineResults, 0, 1);
                        }
                        lRegularBucketIndex = 0;
                        break;
                    case CARRY_OVER_BUCKET:
                        TPAResources.debug(logger, "RuleEngineHandler " ,
                                "sortRuleEngineResults: CarryOverBucket Scenario " ,
                                "with Carry Over Bucket as a Consumption Priority");
                        if (!aInZeroIndexBktInst.getIsCarryOver())
                        {
                            Collections.swap(aInRuleEngineResults, 0, 1);
                        }
                        lRegularBucketIndex = 1;
                        break;
                    default:
                        //do nothing
                        break;
                }


                if (lCarryOverLateRateMode && lRegularBucketIndex != null)
                {
                    // Remove regular bucket by its index after sorting.
                    aInRuleEngineResults.remove((int) lRegularBucketIndex);
                }

            }

            TPAResources.debug(logger, "RuleEngineHandler "
                    , "sortRuleEngineResults: Sorting Successful.");
        }
        else
        {
            TPAResources.debug(logger, "RuleEngineHandler " ,
                    "sortRuleEngineResults: Sorting Failed. This method for " ,
                    "now handles sorting for ", lmaxMultiBuckets,". \n Size " ,
                    "Of RuleEngineResult List is: ", aInRuleEngineResults
                    .size());
        }
    }

    /**
     * Returns true if the rule evaluates to applicable, false if attribute is
     * missing.
     *
     * @param aInTableName table name for applicability condition
     * @param aInContexts source contexts
     * @param aInSourceEntity the source entity to be used if rules
     *                        re-compilation is needed
     * @return true if applicable, false otherwise
     */
    public static boolean isApplicable(String aInTableName,
            Map<String, SourceContext> aInContexts,
            ModifiableEntityIf<?> aInSourceEntity)
    {
        TPAResources.debug(logger,
                "RuleEngineHandler, "
                        , "check Charging Service Applicable with Table Name ",
                aInTableName);
        if (aInTableName == null)
        {
            TPAResources.debug(logger, "RuleEngineHandler : Core Handler, "
                    , "No Applicability Condition : Default True");
            return true;
        }
        // Create the object where results will be stored
        ChargingPolicyDecisionResult lResult =
                new ChargingPolicyDecisionResultImpl(aInSourceEntity);
        // Evaluate the rules
        ChargingPolicyDecisionWorker.evaluateRuleTable(
                "RuleTable:" + aInTableName, RuleSetType.APPLICABILITY,
                aInContexts, lResult);
        // Look for our result
        Boolean lAttributeResult = lResult.getAttributeResult(
                ResultContextType.RATING,
                ChargingRuleAttributeName.CHARGING_SERVICE_APPLICABLE);
        TPAResources.debug(logger,
                "RuleEngineHandler : isApplicable, "
                        , "Result of CS applicable Condition : ",
                lAttributeResult);
        // An applicability attribute was found
        return lAttributeResult != null;
    }

    /**
     * This method initialize parameter required by rating engine module, call rating engine method, process response
     * from rule engine.
     *
     * @param aInChargingServiceDS List of Charging service
     * @param aInRequestWrapper Rating request object received from call control
     * @param aInObjectChargingServices list of all subscription charging services.
     * @return {@link RuleResultWrapper}
     */
    public static RuleResultWrapper ruleEngineLogicIoT(SubscriptionChargingServiceDS aInChargingServiceDS,
            REWrapper aInRequestWrapper, List<SubscriptionChargingServiceDS> aInObjectChargingServices)
    {
        TPAResources.debug(logger, DEVICE_PROFILE_LOGGING_PREFIX, "RuleEngineHandler : " ,
                "ruleEngineLogic Start");
        RatingEngineRequest lReRequest = aInRequestWrapper.getRatingEngineRequest();

        // Identification of Rule Engine from Charging service Definition
        ChargingService lService = aInChargingServiceDS.getChargingServiceDef();
        ChargingServiceInstance lChargingServiceInst = aInChargingServiceDS.getChargingServiceInst();
        String lAppCond = aInChargingServiceDS.getChargingServiceDef().getAppCond();
        // Population of source contexts
        Map<String, SourceContext> lSourceContexts = populateSourceContextMap(aInChargingServiceDS, lReRequest);
        TPAResources.debug(logger, "RatingEngine : Core Handler, Check Applicability condition of " ,
                        "Charging Service : ", lService.getId());
        RuleResultWrapper lReWrapper = null;
        if (!isApplicable(lAppCond, lSourceContexts, lService))
        {
            // This charging service not applicable.
            TPAResources.debug(logger, DEVICE_PROFILE_LOGGING_PREFIX, "RatingEngine : Charging Service" ,
                    " Is Not Applicable.");
            lReWrapper = new RuleResultWrapper();
            lReWrapper.setApplicable(false);
            return lReWrapper;
        }
        String lTariffName = lService.getPasses().get(0).getTariff();
        // Call method to evaluate Rule Engine, method return Account/Bucket information to calculate rate from
        try
        {
            List<PassContext> lPassContexts = new ArrayList<>();
            lReWrapper = triggerRuleEngineForTariff(lSourceContexts, aInChargingServiceDS, lChargingServiceInst,
                    0, aInRequestWrapper, aInObjectChargingServices, lPassContexts);
            lReWrapper.setApplicable(true);
            lReWrapper.setTariffName(lTariffName);
        }
        catch (Exception aInRatingException)
        {
            TPAResources.error(logger, DEVICE_PROFILE_LOGGING_PREFIX, "coreRatingImpl : Core Handler," ,
                    " triggerRuleEngine method exception ", aInRatingException.getMessage());
            (aInRequestWrapper.getRatingEngineRequest().getRatingEngineAnswer()).setResultCode(
                    RatingEngineResultCodes.RULE_ENGINE_ERROR);
            setErrorMessageForRuleEngineErr(lReRequest, lService.getId());
            if (lReWrapper == null)
            {
                lReWrapper = new RuleResultWrapper();
            }
            lReWrapper.setApplicable(false);
            lReWrapper.setDefaultAction(RuleEngineEnum.RULE_ENGINE_ERROR);
            return lReWrapper;
        }

        // for RULE_ENGINE_REJECT case ).
        if (lReWrapper.getDefaultAction() == RuleEngineEnum.RULE_ENGINE_REJECT)
        {
            (aInRequestWrapper.getRatingEngineRequest().getRatingEngineAnswer()).setResultCode(
                    RatingEngineResultCodes.CALL_REJECTED_FROM_TARIFF_FORMULA);
            TPAResources.debug(logger, DEVICE_PROFILE_LOGGING_PREFIX, "Rule Engine Handler set" ,
                    " RatingEngineResultCode CALL_REJECTED_FROM_TARIFF_FORMULA");
            return lReWrapper;
        }
        // If default action is not null and equals to BYPASS continue for next CS (No charging from current CS ).
        if (lReWrapper.getDefaultAction() == RuleEngineEnum.RULE_ENGINE_BYPASS)
        {
            lReWrapper.setDefaultAction(RuleEngineEnum.RULE_ENGINE_BYPASS);
            lReWrapper.setTariffName(lTariffName);
            return lReWrapper;
        }

        if (lReWrapper.getResultListsize() == 0)
        {
            if (GlobalConfig.getLogDetailForErrorResultCode() == LogDetailForErrorResultCode.ENABLED)
            {
                TPAResources.error(logger, DEVICE_PROFILE_LOGGING_PREFIX, "coreRatingImpl : Core Handler,",
                        " RuleEngineResult list empty for device ",
                        aInRequestWrapper.getRatingEngineRequest().getCall().getDeviceContext().getDeviceId(),
                        " and session ", aInRequestWrapper.getRatingEngineRequest().getCall()
                                .getCommonCallContext().getSessionId());
            }
            else
            {
                TPAResources.debug(logger, DEVICE_PROFILE_LOGGING_PREFIX, "coreRatingImpl : Core Handler,",
                        " RuleEngineResult list empty");
            }
            (aInRequestWrapper.getRatingEngineRequest().getRatingEngineAnswer()).
                    setResultCode(RatingEngineResultCodes.RULE_ENGINE_RESULT_EMPTY);
            lReWrapper.setDefaultAction(RuleEngineEnum.RULE_ENGINE_ERROR);
        }
        return lReWrapper;
    }

    /**
     * This method initialize parameter required by rating engine module, call
     * rule engine method to check applicability, process response from rule engine.
     *
     * @param aInChargingServiceDS List of Charging service
     * @param aInRequestWrapper Rating request object received from call control
     * @param aInPassNo Pass Number

     * @return {@link RuleResultWrapper}
     */
    public static RuleResultWrapper ruleEngineApplicabilityLogic(
            SubscriptionChargingServiceDS aInChargingServiceDS,
            REWrapper aInRequestWrapper, int aInPassNo)
    {
        return ruleEngineApplicabilityLogic(aInChargingServiceDS, aInRequestWrapper, aInPassNo, null, null);
    }

    /**
     * This method set Error-Message AVP for RULE_ENGINE_ERROR
     *
     * @param aInReRequest Rating request object received from call control
     * @param aInCSName    Charging Service Name
     */
    private static void setErrorMessageForRuleEngineErr(RatingEngineRequest aInReRequest, String aInCSName)
    {
        TPAResources.debug(logger, "setErrorMessageForRuleEngineErr");
        Long lRatingGroup = null;
        List<Long> lServiceIdentifier = null;
        if (aInReRequest.getCall() != null && aInReRequest.getCall().getGyMessageContext() != null
                && aInReRequest.getCall().getGyMessageContext().getCurrentMscc() != null)
        {
            lRatingGroup = aInReRequest.getCall().getGyMessageContext().getCurrentMscc().getRatingGroup();
            lServiceIdentifier = aInReRequest.getCall().getGyMessageContext().getCurrentMscc().getServiceIdentifier();
        }
        String lErrorMessage = null;
        if (lRatingGroup != null)
        {
            lErrorMessage = "RULE_ENGINE_ERROR For TARIFF rule in Charging Service with ID " + aInCSName + "For RatingGroup " + lRatingGroup;
        }
        else if (lServiceIdentifier != null)
        {
            lErrorMessage = "RULE_ENGINE_ERROR For TARIFF rule in Charging Service with ID " + aInCSName + "For RatingGroup " + lServiceIdentifier;
        }
        ((RatingEngineAnswerImpl) aInReRequest.getRatingEngineAnswer()).setErrorMessageAVP(lErrorMessage);
    }

    /**
     * This method initialize parameter required by rating engine module, call
     * rule engine method to check applicability, process response from rule engine.
     *
     * @param aInChargingServiceDS List of Charging service
     * @param aInRequestWrapper Rating request object received from call control
     * @param aInAccount account entity
     * @param aInGroup Group
     * @param aInPassNo Pass Number
     * @param aInAccount Account entity
     * @param aInGroup Group Entity

     * @return {@link RuleResultWrapper}
     */
    public static RuleResultWrapper ruleEngineApplicabilityLogic(
            SubscriptionChargingServiceDS aInChargingServiceDS,
            REWrapper aInRequestWrapper, int aInPassNo, Account aInAccount,
            Group aInGroup)
    {
        TPAResources.debug(logger, "RuleEngineHandler : ruleEngineApplicabilityLogic Start");
        RatingEngineRequest lReRequest = aInRequestWrapper
                .getRatingEngineRequest();

        RuleResultWrapper lReWrapper =
                new RuleResultWrapper();
        lReWrapper.setApplicable(true);

        // Identification of Rule Engine from Charging service Definition
        ChargingService lService = aInChargingServiceDS.getChargingServiceDef();
        String lAppCond = aInChargingServiceDS.getChargingServiceDef()
                .getAppCond();

        String lRuleTableForCLApplicability = null;
        //applicability Rule evaluation cache Key
        if(null != aInChargingServiceDS.getGroup())
        {
            lRuleTableForCLApplicability = RuleSetType.APPLICABILITY + lService.getId()
                    + aInChargingServiceDS.getGroup().getGroupName();
        }
        else
        {
            lRuleTableForCLApplicability = RuleSetType.APPLICABILITY + lService.getId();
        }

        Map lRuleEvaluationMap = null;
        if (isCSApplicabilityCachingEnabled())
        {
            boolean lIsApplicable = false;
            lRuleEvaluationMap = Optional.ofNullable(lReRequest.getCall()).map(Call::getGyMessageContext)
                                 .map(GyMessageContext::getRuleEvaluationMap).orElse(null);
            if (Objects.nonNull(lRuleEvaluationMap) &&
                    lRuleEvaluationMap.containsKey(lRuleTableForCLApplicability))
            {
                lIsApplicable =
                        (Boolean) lRuleEvaluationMap.get(lRuleTableForCLApplicability);
                TPAResources.debug(logger, "lIsApplicable result From RuleTable cache ",
                        lRuleTableForCLApplicability, " : ", lIsApplicable);
                lReWrapper.setApplicable(lIsApplicable);
                if(lReWrapper.isApplicable())
                {
                    TPAResources.debug(logger, "Updating ApplicabilityState to CS_APPLICABLE");
                    aInRequestWrapper.getContextManagement().setApplicabilityState(ApplicabilityState.CS_APPLICABLE);
                }
                return lReWrapper;
            }
        }

        // Population of source contexts
        Map<String, SourceContext> lSourceContexts = populateSourceContextMap(
                aInChargingServiceDS, lReRequest, aInAccount, aInGroup);
        TPAResources.debug(logger, "RatingEngine : Core Handler, "
                        , "Check Applicability condition of Charging Service : ",
                lService.getId());

        if (aInPassNo == 0 && (!isApplicable(lAppCond, lSourceContexts,
                lService)))
        {
            // This charging service not applicable.
            // TODO don't need to re-evaluate for Miletsone3
            TPAResources.debug(logger, "RatingEngine : Core" , " Handler, "
                    , "Charging Service Is Not Applicable.");
            lReWrapper.setApplicable(false);
            if (isCSApplicabilityCachingEnabled()) {
                setCLApplicabilityConditionInCache(lReRequest, lRuleTableForCLApplicability, false);
            }
        }
        else
        {
            if (isCSApplicabilityCachingEnabled()) {
                setCLApplicabilityConditionInCache(lReRequest, lRuleTableForCLApplicability, true);
            }
        }
        if(lReWrapper.isApplicable())
        {
            TPAResources.debug(logger, "Updating ApplicabilityState to CS_APPLICABLE");
            aInRequestWrapper.getContextManagement().setApplicabilityState(ApplicabilityState.CS_APPLICABLE);
        }
        return lReWrapper;
    }

    private static boolean isCSApplicabilityCachingEnabled()
    {
        CLApplicabilityRuleOptimization lCLApplicabilityRuleOptimization =
                GlobalConfig.getClApplicabilityRuleOptimization();
        TPAResources.debug(logger, "CLApplicabilityRuleOptimization is: ", lCLApplicabilityRuleOptimization);
        return lCLApplicabilityRuleOptimization.isCacheCLApplicabilityEnabled();
    }

    /**
     * This method initialize parameter required by rating engine module, call
     * rule engine method to trigger Tariff rules, process response from rule engine.
     *
     * @param aInChargingServiceDS List of Charging service
     * @param aInRequestWrapper Rating request object received from call control
     * @param aInPassNo Pass Number
     * @param aInObjectChargingServices list of all subscription charging services.
     * @param aInPassContexts list of pass context information of CS's processed so far.
     * @return {@link RuleResultWrapper}
     */
    public static RuleResultWrapper ruleEngineTariffLogic(
            SubscriptionChargingServiceDS aInChargingServiceDS,
            REWrapper aInRequestWrapper, int aInPassNo,
            List<SubscriptionChargingServiceDS> aInObjectChargingServices,
            List<PassContext> aInPassContexts, AtomicBoolean aInIsZeroRate)
    {
        TPAResources.debug(logger, "RuleEngineHandler : ruleEngineTariffLogic Start");
        RatingEngineRequest lReRequest = aInRequestWrapper
                .getRatingEngineRequest();

        // Identification of Rule Engine from Charging service Definition
        ChargingService lService = aInChargingServiceDS.getChargingServiceDef();
        ChargingServiceInstance lChargingServiceInst = aInChargingServiceDS
                .getChargingServiceInst();

        // Population of source contexts
        Map<String, SourceContext> lSourceContexts =
                populateSourceContextMap(aInChargingServiceDS, lReRequest);
        TPAResources.debug(logger, "RatingEngine : Core Handler, "
                        , "Check Applicability condition of Charging Service : ",
                lService.getId());

        RuleResultWrapper lReWrapper = null;
        String lTariffName = null;
        if (!CommonUtil.isNullOrEmpty(lService.getPasses()))
        {
            lTariffName = lService.getPasses().get(aInPassNo).getTariff();
        }
        // Call method to evaluate Rule Engine, method return
        // Account/Bucket information to charge from
        try
        {
            lReWrapper =
                    triggerRuleEngineForTariff(
                            lSourceContexts,aInChargingServiceDS,
                            lChargingServiceInst,aInPassNo,
                            aInRequestWrapper,
                            aInObjectChargingServices,
                            aInPassContexts);
            aInIsZeroRate.set(isZeroRate);
            lReWrapper.setApplicable(true);
            lReWrapper.setTariffName(lTariffName);
        }
        catch (Exception eRatingException)
        {
            TPAResources.error(logger,
                    "coreRatingImpl : Core Handler, "
                            , "triggerRuleEngine method exception ",
                    eRatingException.getMessage());
            ((RatingEngineAnswerImpl) aInRequestWrapper.getRatingEngineRequest()
                    .getRatingEngineAnswer()).setResultCode(
                    RatingEngineResultCodes.RULE_ENGINE_ERROR);
            setErrorMessageForRuleEngineErr(lReRequest, lService.getId());
            if (lReWrapper == null)
            {
                lReWrapper = new RuleResultWrapper();
            }
            lReWrapper.setApplicable(false);
            lReWrapper.setDefaultAction(RuleEngineEnum.RULE_ENGINE_ERROR);
            return lReWrapper;
        }

        // for RULE_ENGINE_REJECT case ).
        if (lReWrapper.getDefaultAction() == RuleEngineEnum.RULE_ENGINE_REJECT)
        {
            ((RatingEngineAnswerImpl) aInRequestWrapper.getRatingEngineRequest()
                    .getRatingEngineAnswer()).setResultCode(
                    RatingEngineResultCodes.CALL_REJECTED_FROM_TARIFF_FORMULA);
            TPAResources.debug(logger, "Rule Engine Handler set RatingEngineResultCode "
                    , "CALL_REJECTED_FROM_TARIFF_FORMULA");
            return lReWrapper;
        }
        // for RULE_ENGINE_TRIGGER_BOU_REJECT case.
        if (lReWrapper.getDefaultAction() == RuleEngineEnum.RULE_ENGINE_TRIGGER_BOU_REJECT)
        {
            ((RatingEngineAnswerImpl) aInRequestWrapper.getRatingEngineRequest()
                    .getRatingEngineAnswer()).setResultCode(
                    RatingEngineResultCodes.CALL_REJECTED_FROM_TARIFF_FORMULA);
            TPAResources.debug(logger, "Rule Engine Handler set RatingEngineResultCode for REJECT"
                    , "CALL_REJECTED_FROM_TARIFF_FORMULA");
            return lReWrapper;
        }
        // If default action is not null and equals to BYPASS continue
        // for next CS (No charging from current CS ).
        if (lReWrapper.getDefaultAction() == RuleEngineEnum.RULE_ENGINE_BYPASS)
        {
            lReWrapper.setDefaultAction(RuleEngineEnum.RULE_ENGINE_BYPASS);
            lReWrapper.setTariffName(lTariffName);
            return lReWrapper;
        }

        if (lReWrapper.getResultListsize() == 0)
        {
            if (GlobalConfig.getLogDetailForErrorResultCode() == LogDetailForErrorResultCode.ENABLED)
            {
                TPAResources.error(logger, "coreRatingImpl : Core Handler, "
                        , "RuleEngineResult list empty for device ",
                        aInRequestWrapper.getRatingEngineRequest().getCall().getDeviceContext().getDeviceId(),
                        " and session ", aInRequestWrapper.getRatingEngineRequest().getCall()
                                .getCommonCallContext().getSessionId());
            }
            else
            {
                TPAResources.debug(logger, "coreRatingImpl : Core Handler, "
                        , "RuleEngineResult list empty");
            }
            ((RatingEngineAnswerImpl) aInRequestWrapper.getRatingEngineRequest()
                    .getRatingEngineAnswer()).setResultCode(
                    RatingEngineResultCodes.RULE_ENGINE_RESULT_EMPTY);
            lReWrapper.setDefaultAction(RuleEngineEnum.RULE_ENGINE_ERROR);
        }
        RatingEngineResultCodes lCurrentResultCode = ((RatingEngineAnswerImpl) aInRequestWrapper
                .getRatingEngineRequest().getRatingEngineAnswer()).getResultCode();

        if(lReWrapper.getDefaultAction() == null &&
                lCurrentResultCode == RatingEngineResultCodes.CALL_REJECTED_FROM_TARIFF_FORMULA)
        {
            ((RatingEngineAnswerImpl) aInRequestWrapper.getRatingEngineRequest()
                    .getRatingEngineAnswer()).setResultCode(
                    RatingEngineResultCodes.CALL_OK);
        }

        return lReWrapper;
    }

    /**
     * This method initialize parameter required by rating engine module, call
     * rating engine method, process response from rul engine.
     *
     * @param aInChargingServiceDS List of Charging service
     * @param aInRequestWrapper Rating request object received from call control
     * @param aInPassNo Pass Number
     * @param aInObjectChargingServices list of all subscription charging services.
     * @param aInPassContexts list of pass context information of CS's processed so far.
     * @return {@link RuleResultWrapper}
     */
    public static RuleResultWrapper ruleEngineLogic(
            SubscriptionChargingServiceDS aInChargingServiceDS,
            REWrapper aInRequestWrapper, int aInPassNo,
            List<SubscriptionChargingServiceDS> aInObjectChargingServices,
            List<PassContext> aInPassContexts)
    {
        TPAResources.debug(logger, "RuleEngineHandler : ruleEngineLogic Start");
        RuleResultWrapper lReWrapper =
                ruleEngineApplicabilityLogic(aInChargingServiceDS,
                        aInRequestWrapper, aInPassNo);
        if(lReWrapper.isApplicable())
        {
            lReWrapper = ruleEngineTariffLogic(
                    aInChargingServiceDS, aInRequestWrapper,
                    aInPassNo, aInObjectChargingServices, aInPassContexts, new AtomicBoolean(false));
        }
        return lReWrapper;
    }

    /**
     * Method to populate source context map
     * @param aInChargingServiceDS         ChargingServiceDS Object
     * @param aInReRequest                 RE Request object
     * @return Source Context map
     */
    public static Map<String, SourceContext> populateSourceContextMap(
            SubscriptionChargingServiceDS aInChargingServiceDS, RatingEngineRequest aInReRequest)
    {
        return populateSourceContextMap(aInChargingServiceDS, aInReRequest, null, null);
    }

    /**
     * Method to populate source context map
     * @param aInChargingServiceDS         ChargingServiceDS Object
     * @param aInReRequest                 RE Request object
     * @param aInAccount                   Account entity
     * @param aInGroup                   Group entity
     * @return Source Context map
     */
    public static Map<String, SourceContext> populateSourceContextMap(
            SubscriptionChargingServiceDS aInChargingServiceDS, RatingEngineRequest aInReRequest,
            Account aInAccount, Group aInGroup)
    {
        Map<String, SourceContext> lOutSourceContexts = new HashMap<>();
        TPAResources.debug(logger, "Populating Source Context for Rule ");
        lOutSourceContexts.put(SourceContextType.CALL_COMMON,
                aInReRequest.getCall().getCommonCallContext());
        lOutSourceContexts.put(SourceContextType.SPR_DEVICE,
                aInReRequest.getCall().getDeviceContext());
        lOutSourceContexts.put(SourceContextType.SPR_USER,
                new UserContextImpl ( ));
        lOutSourceContexts.put(SourceContextType.SPR_GROUP_TRANS,
                new GroupContextTransImpl());
        lOutSourceContexts.put(SourceContextType.SERVICE_INFO,
                aInReRequest.getCall().getServiceInfoContext());
        lOutSourceContexts.put(SourceContextType.GY_MESSAGE,
                aInReRequest.getCall().getGyMessageContext());
        lOutSourceContexts.put(SourceContextType.IMS_SERVICE_INFO,
                aInReRequest.getCall().getImsServiceInformationContext());
        lOutSourceContexts.put(SourceContextType.NCHF_CHARGING_MESSAGE,
                aInReRequest.getCall().getNchfChargingMessageContext());
        lOutSourceContexts.put(SourceContextType.NUMBER_PORTABILITY,
                aInReRequest.getCall().getNumberPortabilityContext());
        lOutSourceContexts.put(SourceContextType.SESSION_PARAMETERS,
                aInReRequest.getCall().getSessionParameterContext());

        if (!CommonUtil.isNullOrEmpty(aInReRequest.getCall().getEcommerceCallContext()))
        {
            lOutSourceContexts.put(SourceContextType.ECOMMERCE,
                    aInReRequest.getCall().getEcommerceCallContext());
        }
        if (aInChargingServiceDS.getSubscription() != null
                && aInChargingServiceDS.getSubscription().getAccount() != null
                && aInChargingServiceDS.getSubscription().getAccount().getId() != null)
        {
            String lAccountId = aInChargingServiceDS.getSubscription().getAccount().getId();
            SubscriptionContextImpl lSubscriptionContext = new SubscriptionContextImpl(lAccountId);
            if (aInChargingServiceDS.getSubscription().getId() != null
                    && aInChargingServiceDS.getChargingServiceDef() != null
                    && aInChargingServiceDS.getChargingServiceDef().getId() != null)
            {
                TPAResources.debug(logger, "Create Subscription Context. Subscription ID = ",
                        aInChargingServiceDS.getSubscription().getId(),
                        " Current Processed CS ID = ", aInChargingServiceDS.getChargingServiceDef().getId());
                lSubscriptionContext.setSubscriptionId(aInChargingServiceDS.getSubscription().getId());
                lSubscriptionContext.setCurProcessCSId(aInChargingServiceDS.getChargingServiceDef().getId());
            }
            if(aInChargingServiceDS.getSubscription().getBundle() != null)
            {
                lSubscriptionContext.setBundleId(aInChargingServiceDS.getSubscription().getBundle().getId());
            }
            lOutSourceContexts.put(SourceContextType.SUBSCRIPTION,lSubscriptionContext);
            // Need to read account from db since not all the info is in the account object attached to subscription
            if (CommonUtil.isNotNullOrEmpty(lAccountId))
            {
                AccountContext lAccountContext = createAccountContextFromAccountId(lAccountId,
                        aInAccount);
                lOutSourceContexts.put(SourceContextType.SPR_ACCOUNT, lAccountContext);
            }
        }
        String lGroupId = Objects.nonNull(aInChargingServiceDS.getGroup())
                        ? aInChargingServiceDS.getGroup().getId() : null;
        GroupContext lGroupContext = createAndUpdateGroupContext(lGroupId, aInReRequest.getCall(), aInGroup);
        lOutSourceContexts.put(SourceContextType.SPR_GROUP,lGroupContext);
        return lOutSourceContexts;
    }

    /**
     * This method validate rule engine result object
     *
     * @param aInRuleEngineResult Rule engine object return by rule engine
     * @param aInRequestWrap RE Request wrapper
     * @return true if valid false if validation fails
     */
    public static boolean validateRuleResult(
            RuleEngineResult aInRuleEngineResult, REWrapper aInRequestWrap)
    {
        TPAResources.debug(logger,
                "RuleEngineHandler : validateRuleResult " , "Start");
        RatingEngineRequest lReRequest = aInRequestWrap
                .getRatingEngineRequest();
        // For charging from Account, rate entity is mandatory, if rate is
        // missing we bypass this rule engine result and continue with next
        // rule engine result
        if ((aInRuleEngineResult.getRateEntityKey() == null)
                && (aInRuleEngineResult.getAccountEntityKey() != null))
        {
            if (GlobalConfig.getLogDetailForErrorResultCode() == LogDetailForErrorResultCode.ENABLED)
            {
                TPAResources.error(logger, "Rate Entity can not be Null for Account. Device ",
                        aInRequestWrap.getRatingEngineRequest().getCall().getDeviceContext().getDeviceId(),
                        " and session ", aInRequestWrap.getRatingEngineRequest()
                                .getCall().getCommonCallContext().getSessionId(), ".");
            }
            else
            {
                TPAResources.debug(logger,
                        "Rate Entity can not be Null for " , "Account");
            }
            ((RatingEngineAnswerImpl) lReRequest.getRatingEngineAnswer())
                    .setResultCode(
                            RatingEngineResultCodes.RULE_ENGINE_RATE_ID_MISSING_FOR_ACCOUNT);
            return false;
        }

        // Both account and bucket key must not be present in same result
        // from rule engine
        if ((aInRuleEngineResult.getAccountEntityKey() == null) &&
                (aInRuleEngineResult.getBucketEntityKey() == null) &&
                aInRuleEngineResult.getCallDiscount() == null &&
                Boolean.FALSE.equals(aInRuleEngineResult.getNoCharge()))
        {
            if (GlobalConfig.getLogDetailForErrorResultCode() == LogDetailForErrorResultCode.ENABLED)
            {
                TPAResources.error(logger, "Account entity, bucket and call-discount ",
                        "can not all be null in one RuleEngineResult for device ",
                        aInRequestWrap.getRatingEngineRequest().getCall().getDeviceContext().getDeviceId(),
                        " and session ", aInRequestWrap.getRatingEngineRequest()
                                .getCall().getCommonCallContext().getSessionId(), ".");
            }
            else
            {
                TPAResources.debug(logger, "Account Entity, Bucket and Call-Discount can not "
                        , "be Null in one RuleEngineResult.");
            }
            ((RatingEngineAnswerImpl) lReRequest.getRatingEngineAnswer())
                    .setResultCode(
                            RatingEngineResultCodes.RULE_ENGINE_RESULT_EMPTY);
            // aInRequestWrap.getContextManagement().
            // setExitCsLoop(true);
            return false;
        }

        // Rule engine result must contain reference either bucket or bundle
        // both should not be null
        if ((aInRuleEngineResult.getAccountEntityKey() != null)
                && (aInRuleEngineResult.getBucketEntityKey() != null))
        {
            if (GlobalConfig.getLogDetailForErrorResultCode() == LogDetailForErrorResultCode.ENABLED)
            {
                TPAResources.error(logger, "Account entity and bucket can not both be present ",
                        "in same RuleEngineResult for device ",
                        aInRequestWrap.getRatingEngineRequest().getCall().getDeviceContext().getDeviceId(),
                        " and session ", aInRequestWrap.getRatingEngineRequest()
                                .getCall().getCommonCallContext().getSessionId(), ".");
            }
            else
            {
                TPAResources.debug(logger, "Account Entity and Bucket both"
                        , " can not be present in same RuleEngineResult.");
            }
            ((RatingEngineAnswerImpl) lReRequest.getRatingEngineAnswer())
                    .setResultCode(
                            RatingEngineResultCodes.RULE_ENGINE_RESULT_NOT_VALID);
            // aInRequestWrap.getContextManagement().
            // setExitCsLoop(false);
            return false;
        }
        return true;

    }

    /**
     * Validate Next Rate rule result info got from tariff.
     *
     * @param aInNextRateRuleEngineResult {@link RuleEngineResult} for
     *                                    Next-Rate.
     * @param aInRequestWrap              {@link REWrapper} instance.
     * @return true if no validation error exists.
     */
    public static boolean validateNextRateRuleEngineResult(
            RuleEngineResult aInNextRateRuleEngineResult,
            REWrapper aInRequestWrap)
    {
        if (aInNextRateRuleEngineResult != null)
        {
            TPAResources.debug(logger, "NextRateRuleEngineResult found :" ,
                    " validateNextRateRuleEngineResult Start");
            if (!validateRuleResult(aInNextRateRuleEngineResult,
                    aInRequestWrap))
            {
                return false;
            }

        }
        return true;
    }

    /**
     * This Method trigger rule engine to fetch rule engine output to use it for
     * further processing related to threshold and notification
     *
     * @param aInREWrapper Rating Engine Request wrapper as input.
     * @param aInThrshldPrfContext Threshold profile Context
     * @param aInIsCap Threshold triggered from cap set this as true
     * @param aInCounterInstanceClass {@link CounterInstanceClass}
     * @param aInEntityCounterInstance EntityCounterInstance
     * @param aInSubscriptionContext Subscription Context
     * @param aInReserveCommit to indicate if Reservation/Commit call
     * @param aInEntityInformation Entity Information'
     * @param aInAVInstance Av whose threshold actions
     * @param aInAccount Account for fetching users for notifications
     * @param aInThresholdType Type of Threshold - Reservation or Commit
     * @param aInThresholdInstanceMap Map containing threshold name as key and instance as value
     * @param aInThresholdProfile threshold profile definition from DB
     * @return ThresholdRuleResultWrapper List of Rule Engine result for further
     *         actions.
     * @throws OperationFailedException
     */
    public static ThresholdRuleResultWrapper triggerRuleEngineForThreshold(
            REWrapper aInREWrapper,
            ThresholdProfileContextImpl aInThrshldPrfContext, Boolean aInIsCap,
            CounterInstanceClass aInCounterInstanceClass,
            EntityCounterInstance aInEntityCounterInstance,
            SubscriptionContext aInSubscriptionContext,
            RatingCallActions aInReserveCommit,
            EntityInformation aInEntityInformation, AVInstance aInAVInstance,
            Account aInAccount, ThresholdType aInThresholdType,
            BucketInstance aInBucketInstance,
            Map<String, ThresholdInstance> aInThresholdInstanceMap,
            ThresholdProfile aInThresholdProfile)
    {
        TPAResources.debug(logger, "triggerRuleEngineForThreshold Start");

        EntityCounterInstanceId lEntityCounterInstanceId = null;
        if (aInEntityCounterInstance != null)
        {
            lEntityCounterInstanceId = aInEntityCounterInstance.getId();
        }
        ThresholdRuleResultWrapper lThrRuleResultWrapper = new ThresholdRuleResultWrapper();
        RatingEngineRequest lRatingEngineRequest = aInREWrapper.getRatingEngineRequest();
        Call lCall = lRatingEngineRequest.getCall();
        CommonCallContext lCCContext = lCall.getCommonCallContext();
        DeviceContext lDeviceContext = lCall.getDeviceContext();
        //Update Counter information into Device Context.
        updateCounterInfoInDeviceContext(lDeviceContext, aInEntityCounterInstance);
        ServiceInfoContext lServiceInfoContext = lCall.getServiceInfoContext();
        GyMessageContext lGyMessageContext = lCall.getGyMessageContext();
        ImsServiceInformationContext lImsServiceInformationContext = lCall
                .getImsServiceInformationContext();
        NchfChargingMessageContext lNchfChargingMessageContext = lCall
                .getNchfChargingMessageContext();
        SessionParameterContext lSessionParameterContext = lCall.getSessionParameterContext();
        ECommerceCallContext lECommerceCallContext = lCall.getEcommerceCallContext();
        GroupContext lGroupContext =
                createAndUpdateGroupContext(aInEntityCounterInstance, getGroupId
                        (lEntityCounterInstanceId, aInEntityInformation,
                                aInAVInstance), lCall);

        if (aInSubscriptionContext == null)
        {
            aInSubscriptionContext = EntityCounterUtility.createSubscriptionContext(
                    lCall, aInEntityCounterInstance, lEntityCounterInstanceId,
                    aInEntityInformation, aInREWrapper, null,
                    aInCounterInstanceClass);
        }

        String lAccountId = findAccountIdForThresholdTrigger(
                aInAccount, aInSubscriptionContext, aInEntityCounterInstance);

        AccountContext lAccountContext = createAccountContextFromAccountIdWithSecondaryBalance(lAccountId ,
                aInREWrapper.getContextManagement().getSecondaryBalanceChrgSrvDSList());

        ChargingPolicyDecisionResult lThrResult = getRuleResultForThreshold(lCCContext,
                lDeviceContext, aInThrshldPrfContext, lServiceInfoContext, lThrRuleResultWrapper,
                aInSubscriptionContext, lGyMessageContext, lImsServiceInformationContext,
                lNchfChargingMessageContext,lGroupContext,lECommerceCallContext,lAccountContext, new UserContextImpl ( ) , aInREWrapper.getContextManagement().getEntityInformation(),
                lSessionParameterContext);
        if (lThrResult == null)
        {
            return lThrRuleResultWrapper;
        }
        if (lThrResult
                .getContextResults(ResultContextType.SESSION_PARAMETERS) !=
                null)
        {
            //Add-CustomField-In-Session Action
            handleAddCustomFieldFromResultContext(lThrResult, lRatingEngineRequest);
        }
        //for CCRi ,Account threshold rule just need Announcement related VSA
        if(aInReserveCommit.equals(RatingCallActions.RESERVE)
                && aInThrshldPrfContext.getThresholdEntityType() != null
                && aInThrshldPrfContext.getThresholdEntityType().equals(ThresholdEntityType.MAIN_BALANCE) &&
                !lRatingEngineRequest.getExternalUsageData().isSuppressNotifAndAnn())
        {
            AnnouncementUtility.getAnnounceVSAFromThrRuleResult(lThrResult);
        }
        String lRuleTableName = aInThrshldPrfContext.getProfileRuleTableName();
        // Store the evaluation result in RatingEngineAnswer for VSA processing
        RatingEngineAnswer lReAnswer = lRatingEngineRequest.getRatingEngineAnswer();
        if (!lRatingEngineRequest.getExternalUsageData().isSuppressNotifAndAnn())
        {
            RuleEngineHandler.addPolicyDecisionResultToAnswer(lRuleTableName, lThrResult, lReAnswer);
        }
        else
        {
            TPAResources.debug(logger, "The threshold announcement for trusted number calls has been ",
                    "suppressed for device id [", aInREWrapper.getDeviceId() , "]");
        }
        //for CCRi ,Account threshold rule No need to update Account
        //for CCRi ,Account threshold rule No need to do further actions, only Announcement VSA is needed
        if(aInReserveCommit.equals(RatingCallActions.RESERVE)
                && aInThrshldPrfContext.getThresholdEntityType() != null
                && aInThrshldPrfContext.getThresholdEntityType().equals(ThresholdEntityType.MAIN_BALANCE))
        {
            if (lGyMessageContext != null)
            {
                addActionAVP(lThrResult, lGyMessageContext, lDeviceContext.getDevice(), aInAccount,
                        lRatingEngineRequest.getCdrEngine(), lGyMessageContext.isFUIInMscc(), null, aInREWrapper.getRatingEngineRequest() );
            }
            return lThrRuleResultWrapper;
        }
        // Handle some special actions related to threshold
        processResultForThresholdContext(lThrResult, lThrRuleResultWrapper,aInThrshldPrfContext);
        if (ThreadLocalCommonContext.getIsGlobalGroupResouceAndCrossSiteCall() &&
            lThrRuleResultWrapper.getThresholdRuleEngineResultList() != null &&
            !lThrRuleResultWrapper.getThresholdRuleEngineResultList().isEmpty())
        {
            aInREWrapper.getRatingEngineRequest().getRatingEngineAnswer().setGlobalGroupRedirect(true);
            TPAResources.debug(logger, CALL_FORWARD, "due to crossed threshold");
            return lThrRuleResultWrapper;
        }
        // Store threshold discounts configured in rules in entity instances
        storeThresholdDiscountsInEntity(lThrRuleResultWrapper.getThresholdRuleEngineResultList(),
                aInThrshldPrfContext, aInCounterInstanceClass, aInBucketInstance);
        //Add auto install subscription from Threshold rule result.
        AutoInstallBundleHandler.handleAutoInstallSubscriptionForThreshold(
                (RatingEngineRequestImpl) lRatingEngineRequest, lThrResult,
                aInEntityInformation, aInCounterInstanceClass,
                aInEntityCounterInstance);
        boolean lIsCheckbalanceCall = RatingEngine
                .isCheckBalanceCall(lCCContext.getRequestType(), lCCContext.getRequestAction());
        String lPolicyCtrsState = null;
        //Do not execute below logic for check-balance call
        if (!lIsCheckbalanceCall)
        {
            //Execute custom Data Action
            CustomDataActionUtils
                    .checkAndExecuteCustomDataAction(lDeviceContext, lGroupContext, lThrResult, lCCContext);

            if ((aInSubscriptionContext != null) && (aInSubscriptionContext.getSubscriptionId() != null))
            {
                Subscription lSubscription = Services.lookup(SubscriptionService.class)
                    .read(aInSubscriptionContext.getSubscriptionId());

                CustomDataActionUtils.checkAndExecuteCustomDataActionInSubscription(lSubscription, lThrResult);
            }

            if (lGyMessageContext != null)
            {
                if ((aInThrshldPrfContext.getThresholdEntityType() ==
                        ThresholdEntityType.COUNTER) &&
                        ThresholdExecutionUtility.isPreCallOfIMS(aInREWrapper))
                {
                    //handling 3gpp announcement action in counter threshold rule
                    handleStandardAnnouncementResult(
                            lReAnswer, aInEntityCounterInstance,
                            aInCounterInstanceClass, aInThrshldPrfContext,
                            lThrRuleResultWrapper, lThrResult,
                            lDeviceContext.getDevice());
                    //set counter info for all counter threshold announcements(3gpp or VSA)
                    prepareCounterInfoForAnnouncement(lReAnswer, aInEntityCounterInstance,
                            aInCounterInstanceClass, aInThrshldPrfContext,
                            lThrRuleResultWrapper);
                }
                else
                {
                    addActionAVP(lThrResult, lGyMessageContext,
                            Services.lookup(DeviceService.class).read(lDeviceContext.getDeviceId()),
                            getAccountWithBalance(lAccountId),
                            lRatingEngineRequest.getCdrEngine(), lGyMessageContext.isFUIInMscc(), aInSubscriptionContext, aInREWrapper.getRatingEngineRequest());
                }
            }
            else if (lECommerceCallContext != null
                    && aInThrshldPrfContext.getThresholdEntityType() != null
                    && aInThrshldPrfContext.getThresholdEntityType().equals(ThresholdEntityType.MAIN_BALANCE)
                    && lThrResult.getContextResults(ResultContextType.CALL_RESULT) != null)
            {
                if(GlobalConfig.get5GDeviceIdentitiesFlagInReports() == 1)
                {
                    EDRUtility.addAVPToEDR(lThrResult, lDeviceContext.getDevice(),
                            aInSubscriptionContext);
                }else{
                    EDRUtility.addAVPToEDR(lThrResult, getAccountWithBalance(lAccountId),
                            aInSubscriptionContext);
                }
            }

            String lPolicyCtrsStateFromRule =  ((ChargingPolicyDecisionResult)lThrResult)
                    .getAttributeResult( SY_RESULT_CONTEXT ,
                            ChargingRuleAttributeName.SEND_SNR.toString());
            // process intermediate thresholds
            lPolicyCtrsState =
                    processIntermediateThresholdsAndGetSignalingState(aInThrshldPrfContext,
                            aInThresholdInstanceMap, aInThresholdProfile, lThrResult,
                            lPolicyCtrsStateFromRule);
        }
        // process call_result.add-CDR
        processAddCdrFromRule(lThrResult, lRatingEngineRequest.getCdrEngine());

        //Trigger Subscription Exhaustion event if threshold EXHAUSTION action is configured
        List<GenericValueWrapper> lNotificationExhaust = ((ChargingPolicyDecisionResult)lThrResult)
                .getAttributeResult(ResultContextType.NOTIFICATION,
                        ChargingRuleAttributeName.EXHAUSTION);
        ExhaustEntityType lExhaustEntityType =
                getExhaustEntityType(lNotificationExhaust);
        setAccountIdInRatingEngineAnswer(aInREWrapper, aInEntityInformation,
                aInAccount, aInEntityCounterInstance);
        triggerSubscriptionExhaustionEvent(
                (aInREWrapper.getRatingEngineRequest().getRatingEngineAnswer()).getAccountId(),
                aInCounterInstanceClass,
                aInEntityInformation, lDeviceContext.getDeviceId(), lExhaustEntityType);

        // Fetching the Result for Trigger-Life-Cycle-Event Action
        List<GenericValueWrapper> lLifeCycleTriggerEvents = lThrResult
                .getAttributeResult(ResultContextType.NOTIFICATION, ChargingRuleAttributeName.TRIGGER_LIFE_CYCLE_EVENT);

        if (!CommonUtil.isNullOrEmpty(lLifeCycleTriggerEvents))
        {
            // Triggering life cycle event if Trigger-Life-Cycle-Event action is configured
            EntityIdModel lEntityIdModel =
                    new EntityIdModel(getSubscriptionId(aInCounterInstanceClass, aInEntityInformation),
                            lDeviceContext.getDeviceId(),
                            aInREWrapper.getRatingEngineRequest().getRatingEngineAnswer().getAccountId(),
                            lDeviceContext.getOwnerId(), Objects.isNull(lGroupContext) ? null : lGroupContext.getGroupId());

            for (GenericValueWrapper lWrapper : lLifeCycleTriggerEvents)
            {
                String lTriggerEventName = lWrapper.getName();
                if (StringUtils.isBlank(lTriggerEventName))
                {
                    TPAResources.debug(logger, SKIP_TRIGGER_EVENT,
                            IS_NULL_EMPTY);
                    continue;
                }

                TriggerLifeCycleEntityType lEntityType =
                        (TriggerLifeCycleEntityType) getConverter().extractFromGeneric(lWrapper.getValue());
                triggerLifeCycleEvent(lTriggerEventName, lEntityIdModel, lEntityType);
            }
        }

        TPAResources.debug(logger, "After triggerLifeCycleEvent is ",
                aInEntityCounterInstance);

        if (aInEntityCounterInstance != null &&
                aInEntityCounterInstance.getCounterInstanceList() != null)

        {
            EntityCounterInstance aInNewEntityCounterInstance = Services.lookup(EntityCounterInstanceService.class)
                    .read(aInEntityCounterInstance.getId());
            TPAResources.debug(logger,
                    "After triggerLifeCycleEvent After lookup is aInNewEntityCounterInstance ", aInNewEntityCounterInstance);
            if (aInNewEntityCounterInstance != null &&
                    aInNewEntityCounterInstance.getCounterInstanceList() != null &&
                    aInNewEntityCounterInstance.getCounterInstanceList().size() >
                            aInEntityCounterInstance.getCounterInstanceList().size())

            {
                aInEntityCounterInstance = aInNewEntityCounterInstance;
            }

            TPAResources.debug(logger,
                    "After triggerLifeCycleEvent After lookup is ", aInEntityCounterInstance);
        }

        if (Objects.nonNull(lPolicyCtrsState))
        {
            boolean lResourceBucket = Objects.nonNull(aInEntityInformation) &&
                    aInEntityInformation.getEntityType() == EntityTypeEnum.ENTITY_BUCKET ;

            if (lResourceBucket)
            {
                if (VirtualPolicyCounter.BUCKET_COUNTER.equals(
                        GlobalConfig.getVirtualPolicyCounter()))
                {
                    processCountersForBucket(lPolicyCtrsState,
                            lRatingEngineRequest,
                            aInThrshldPrfContext.getBucketDefName(),
                            aInEntityInformation.getEntityKey(),
                            aInEntityInformation.getSubInstId());
                }
                else
                {
                    processCountersForAvOrBucket(lGroupContext, lDeviceContext,
                            DEVICE.equals(aInEntityInformation.getGroupId()),
                            lPolicyCtrsState, lRatingEngineRequest,
                            aInThrshldPrfContext.getBucketDefName());
                }
            }
            else if (Objects.nonNull(aInAVInstance))
            {
                processCountersForAvOrBucket(lGroupContext, lDeviceContext,
                        (aInAVInstance.getGroup()==null), lPolicyCtrsState,
                        lRatingEngineRequest,
                        aInThrshldPrfContext.getBucketDefName());
            }
            else if (aInCounterInstanceClass != null &&
                    aInCounterInstanceClass.isSyPolicyCounter())
            {
                String lSubsId = EntityCounterUtility.
                        getSubscriptionId(lEntityCounterInstanceId);
                fillSyResultAnswerInfo(lRatingEngineRequest, lPolicyCtrsState,
                        aInCounterInstanceClass, lEntityCounterInstanceId,
                        false, lSubsId);
            }
        }

        // Send Notification logic
        if (checkForSendNotification(aInThrshldPrfContext, aInReserveCommit,
                aInThresholdType) && !lIsCheckbalanceCall)
        {
            Set<String> lEntityIdSet = new HashSet<>();
            String lEntityType = null;
            if (aInAccount != null)
            {
                Boolean isDiableUserConfig =
                        lThrResult
                                .getAttributeResult(ResultContextType.THRESHOLD,
                                        ChargingRuleAttributeName.THRESHOLD_DISABLE_USER_CONFIGURATION);
                TPAResources.debug(logger,
                        "Disable-User-Configuration flag value is  ",
                        isDiableUserConfig);
                if (isDiableUserConfig != null && isDiableUserConfig)
                {
                    TPAResources.debug(logger,
                            "RuleEngineHandler :: " ,
                                    "User configuration for account is disabled ,getting device/group entity set ");
                    String[] lEntitySet = ThresholdHandler.getDeviceGroupEntity(aInAccount.getId());
                    if (lEntitySet[0] != null)
                    {
                        lEntityIdSet.add(lEntitySet[0]);
                        lEntityType = lEntitySet[1];
                    }
                }
                else
                {
                    lEntityIdSet = getDeviceIdSetForAccount(aInAccount);
                    lEntityType = Device.class.getSimpleName();
                }
            }
            else
            {
                String lDeviceId = lDeviceContext.getId();
                String lGroupId = null;
                if (aInCounterInstanceClass == null ||
                        !aInCounterInstanceClass.isSessionVirtualCounter())
                {
                    lGroupId = getGroupId(lEntityCounterInstanceId, aInEntityInformation,
                            aInAVInstance, lThrResult);
                }
                String lEntityId = Objects.nonNull(lGroupId) ? lGroupId : lDeviceId;
                lEntityType = Objects.nonNull(lGroupId)
                        ? Group.class.getSimpleName() : Device.class.getSimpleName();
                lEntityIdSet.add(lEntityId);
            }
            if (lEntityIdSet == null || lEntityIdSet.isEmpty())
            {
                TPAResources.debug(logger,
                        "RuleEngineHandler :: " , "EntityId Set is null");
                return lThrRuleResultWrapper;
            }
            NotificationCollectionImpl lNotificationCollection =
                    (NotificationCollectionImpl) lRatingEngineRequest.getNotificationCollection();
            if (aInIsCap)
            {
                processResultForCapNotifContext(lNotificationCollection, lThrResult,
                        lThrRuleResultWrapper, lEntityIdSet.iterator().next(), lEntityType,
                        aInREWrapper, aInThrshldPrfContext);
            }
            else
            {
                processResultForNotifContext(lNotificationCollection, lThrResult,
                        lThrRuleResultWrapper, lEntityType, lEntityIdSet, aInREWrapper,
                        aInThrshldPrfContext, aInCounterInstanceClass);
            }
        }
        return lThrRuleResultWrapper;
    }

    /**
     * Update PolicyCounterInfo of PCI into DB for Policy Counters state
     * changed.
     *
     * @param aInCounterInstanceClass  Counter Instance object.
     * @param aInEntityCounterInstance Current updated entity counters
     * @param aInSubscriptionInstId    subscription id
     * @param aInDevice                current Device for which call was
     *                                 received.
     * @param aInCurrentState          current SNR state of pol counter.
     * @return Old Signaling State
     */
    public static String updatePolicyCounterInstanceList(
            CounterInstanceClass aInCounterInstanceClass,
            EntityCounterInstance aInEntityCounterInstance,
            String aInSubscriptionInstId, Device aInDevice,
            String aInCurrentState)
    {
        Subscription lSubscription = null;
        String lOldSignalingState = null;
        TPAResources.debug(logger,
                "Updating policy counter instance for CounterInstance: ",
                aInCounterInstanceClass);

        if (aInCurrentState == null)
        {
            TPAResources.debug(logger,
                    "Current state is null, not updating policy counter instance");
            return lOldSignalingState;
        }
        if (aInSubscriptionInstId == null)
        {
            aInSubscriptionInstId = aInCounterInstanceClass.getSubsId();
        }

        if (aInSubscriptionInstId != null)
        {
            lSubscription = getSubscription(aInSubscriptionInstId);
        }
        //Check for Counter name and state.
        Map.Entry<String, Pair<String, String>> lCtrNameAndState =
                getCtrNameAndState(aInCurrentState);
        String lCtrName = null;
        String lCtrSignalState = null;
        if (lCtrNameAndState != null)
        {
            lCtrName = lCtrNameAndState.getKey().trim();
            lCtrSignalState = lCtrNameAndState.getValue().first.trim();
        }

        try
        {
            String lPCIKey = PolicyCounterInstanceUtility
                    .getPCIKey(aInEntityCounterInstance,
                            lSubscription);
            PolicyCounterInstance lPolicyCounterInstanceDB =
                    (PolicyCounterInstance) ThreadLocalCommonContext
                            .getPolicyCtrInsMap().get(lPCIKey);
            if (lPolicyCounterInstanceDB == null)
            {
                PolicyCounterInstanceService lPolicyCounterInstanceService =
                        Services.lookup(PolicyCounterInstanceService.class);
                lPolicyCounterInstanceDB = lPolicyCounterInstanceService
                        .readPolicyCounterInstance(aInEntityCounterInstance,
                                lSubscription);
            }
            if (lPolicyCounterInstanceDB != null)
            {
                for (PolicyCounterInfo lPolicyCounterInfo : lPolicyCounterInstanceDB
                        .getCurrentPolicyCounterInfos())
                {
                    if (aInCounterInstanceClass.getCounterInsId()
                            .equals(lPolicyCounterInfo.getCtrInsId()))
                    {
                        TPAResources.debug(logger,
                                "PolCounterInfo to be updated : ",
                                lPolicyCounterInfo);
                        lPolicyCounterInfo.setLastSNRReportingTime(
                                ThreadLocalCommonContext
                                        .getCurrentTimeStamp());
                        /* oldSNRReportingState must not be updated when application preference
                        'Policy Counter Notification Rule' is set to PREVIOUS_STATE_VPC since old
                        states are synced for all same name VPCs/physical counters after sending
                        the SNR. Hence, old SNR reporting state from DB must be used while
                        evaluating RSV Policy Counter Status Change trigger. */
                        if (GlobalConfig.getPolicyCounterNotificationRule() ==
                                PolicyCounterNotificationRule.PREVIOUS_STATE_COUNTER)
                        {
                            if (lPolicyCounterInfo.getOldSNRReportingState() != null)
                            {
                                lPolicyCounterInfo.setOldSNRReportingState
                                        (lPolicyCounterInfo
                                                .getLastSNRReportingState());
                            }
                            lOldSignalingState = lPolicyCounterInfo.getOldSNRReportingState();
                        }
                        TPAResources.debug(logger, "OldSignaling State : ",
                                lOldSignalingState);
                        lPolicyCounterInfo
                                .setLastSNRReportingState(aInCurrentState);
                        if (lCtrName != null)
                        {
                            lPolicyCounterInfo
                                    .setPolicyCounterName(lCtrName);
                        }

                        if (lCtrSignalState != null)
                        {
                            lPolicyCounterInfo
                                    .setLastSNRReportingState(
                                            lCtrSignalState);
                        }
                        PolicyCounterInstanceUtility.updateLastSNRReportingCtrFlagInPCIObject(
                                lPolicyCounterInstanceDB,
                                aInCounterInstanceClass, null, aInDevice);
                        TPAResources.debug(logger,
                                "Update PolCounterInfo into DB : ",
                                lPolicyCounterInfo);
                        ThreadLocalCommonContext
                                .setPolicyCtrIns(lPCIKey, lPolicyCounterInstanceDB);
                        break;
                    }
                }
            }
            else
            {
                TPAResources.debug(logger, "PolCounterInstance does not exist" ,
                        " in DB to update LastSNRReportingState");
            }
        }
        catch (OperationFailedException aInException)
        {
            TPAResources.errorException(logger, aInException,
                    "Unable to update PolicyCounterInstance in DB's PolicyCounterInstaList");
        }

        return lOldSignalingState;
    }

    /**
     * This method will parse the Counter state and name.
     *
     * @param aInCtrSNRState this string will contain a string with comma
     *                       separated for counter-name nad state.
     * @return counter name and state as  Map.Entry.
     */
    private static Map.Entry<String, Pair<String, String>> getCtrNameAndState(
            String aInCtrSNRState)
    {
        Map<String, Pair<String, String>> lCountersForSNR =
                PolicyCounterUtility.parseInputCounters(aInCtrSNRState);
        if (lCountersForSNR != null && !lCountersForSNR.isEmpty())
        {
            TPAResources.debug(logger,
                    "VPC available to update in PCI list");
            Map.Entry<String, Pair<String, String>> lCounterEntry =
                    lCountersForSNR.entrySet().iterator().next();
            return lCounterEntry;
        }
        return null;
    }

    /**
     * store threshold discounts in entity instance
     *
     * @param aInThresholdRuleEngineResults List of ThresholdRuleEngineResults
     * @param aInThrshldPrfContext          ThresholdProfileContextImpl
     * @param aInCounterInstanceClass       CounterInstanceClass
     * @param aInBucketInstance             BucketInstance
     */
    private static void storeThresholdDiscountsInEntity(List<ThresholdRuleEngineResult> aInThresholdRuleEngineResults,
            ThresholdProfileContextImpl aInThrshldPrfContext, CounterInstanceClass aInCounterInstanceClass,
            BucketInstance aInBucketInstance)
    {
        if (CommonUtil.isNullOrEmpty(aInThresholdRuleEngineResults))
        {
            return;
        }
        Map<String, BigDecimal> lThresholdDiscountMap;
        String lInstanceId = null;
        String lDefinitionId = null;
        if (ThresholdEntityType.COUNTER == aInThrshldPrfContext.getThresholdEntityType())
        {
            lThresholdDiscountMap = aInCounterInstanceClass.getThresholdDiscounts();
            lInstanceId = aInCounterInstanceClass.getCounterInsId();
            lDefinitionId = aInCounterInstanceClass.getCounterDefId();
        }
        else if (ThresholdEntityType.BUCKET == aInThrshldPrfContext.getThresholdEntityType())
        {
            lThresholdDiscountMap = aInBucketInstance.getThresholdDiscounts();
            lInstanceId = aInBucketInstance.getId();
            lDefinitionId = aInBucketInstance.getBucketDefId();
        }
        else
        {
            return;
        }
        for (ThresholdRuleEngineResult lThresholdRuleEngineResult : aInThresholdRuleEngineResults)
        {
            String lThrshldPrfKey =
                    aInThrshldPrfContext.getThresholdProfileGroupName() + SPSCommonConstants.APP_INFO_DELIMITER +
                            aInThrshldPrfContext.getThresholdProfileType().name();
            // find key (TPGName.ProfileType.ThresholdName) in existing Counter instance class's threshold discount map
            // and remove it.
            List<String> lThresholdName = lThresholdDiscountMap.keySet().stream()
                    .filter(lTpgName -> lTpgName.contains(lThrshldPrfKey)).collect(Collectors.toList());
            if (!CommonUtil.isNullOrEmpty(lThresholdName))
            {
                BigDecimal lOldDiscount = lThresholdDiscountMap.get(lThresholdName.get(0));
                lThresholdDiscountMap.remove(lThresholdName.get(0));
                TPAResources.debug(logger,
                        "Threshold Discount = TPG Name [" , aInThrshldPrfContext.getThresholdProfileGroupName() ,
                                "] , threshold profile type [" , aInThrshldPrfContext.getThresholdProfileType().name() ,
                                "], threshold name [" ,
                                lThresholdName.get(0).split(SPSCommonConstants.APP_INFO_DELIMITER)[2] ,
                                "], discount [" , lOldDiscount ,
                                "] removed from entity instance id [" , lInstanceId , "] " , ", definition id [" ,
                                lDefinitionId , "]");
            }

            if (ThresholdDiscountUtility.validateDiscountValue(lThresholdRuleEngineResult.getDiscount()))
            {
                // add discount with TPGName.ProfileType.ThresholdName in Counter Instance class's threshold discount map
                lThresholdDiscountMap.put(lThrshldPrfKey + SPSCommonConstants.APP_INFO_DELIMITER +
                        lThresholdRuleEngineResult.getThresholdName(), lThresholdRuleEngineResult.getDiscount());
                TPAResources.debug(logger,
                        "Threshold Discount = TPG Name [" , aInThrshldPrfContext.getThresholdProfileGroupName() ,
                                "] , threshold profile type [" , aInThrshldPrfContext.getThresholdProfileType().name() ,
                                "], threshold name [" , lThresholdRuleEngineResult.getThresholdName() ,
                                "], discount [" , lThresholdRuleEngineResult.getDiscount() ,
                                "] added in entity instance id [" , lInstanceId , "] " , ", definition id [" ,
                                lDefinitionId , "]");
            }
            else
            {
                TPAResources.debug(logger,
                        "Threshold Discount = TPG Name [" , aInThrshldPrfContext.getThresholdProfileGroupName() ,
                                "] , threshold profile type [" , aInThrshldPrfContext.getThresholdProfileType().name() ,
                                "], threshold name [" , lThresholdRuleEngineResult.getThresholdName() ,
                                "], discount [" , lThresholdRuleEngineResult.getDiscount() ,
                                "] is not added in entity instance id [" , lInstanceId , "] " , ", definition id [" ,
                                lDefinitionId , "], reason is discount should be in range 0-100");
            }
        }
    }

    /**
     * This function will return the {@link GenericValueConverter} object having application name as Charging, so that
     * Generic Value can be converter to the value.
     *
     * @return GenericValueConverter Object
     */
    public static GenericValueConverter getConverter()
    {
        // Setting up the converter to convert the Generic Value to Enum defined
        GenericValueConverter lConverter = new GenericValueConversionUtility();
        lConverter.setApplication(ServiceModelConstants.CHARGING_PDF_APPLICATION);
        return lConverter;
    }

    /**
     * This method is used to trigger Custom or predefined LifeCycle event on {@link TriggerLifeCycleEntityType}
     * attributes.
     *
     * @param aInEventName      LifeCycle Trigger Event Name
     * @param aInEntityIdModel  EntityIdModel reference
     * @param aInEntityType     {@link TriggerLifeCycleEntityType} on which lifecycle should be triggered
     */
    public static void triggerLifeCycleEvent(String aInEventName,
            EntityIdModel aInEntityIdModel,
            TriggerLifeCycleEntityType aInEntityType)
    {
        if (aInEntityType == null || aInEntityIdModel == null)
        {
            return;
        }

        switch (aInEntityType)
        {
            case SUBSCRIPTION:
                BeanContext.lookup(LifeCycleTrigger.class)
                        .triggerLifeCycleEventOnSubscription(aInEntityIdModel.getSubscriptionId(), aInEventName, false);
                break;
            case DEVICE:
                BeanContext.lookup(LifeCycleTrigger.class)
                        .triggerLifeCycleEventOnDevice(aInEntityIdModel.getDeviceId(), aInEventName);
                break;
            case ACCOUNT:
                BeanContext.lookup(LifeCycleTrigger.class)
                        .triggerLifeCycleEventOnAccount(aInEntityIdModel.getAccountId(), aInEventName, false);
                break;
            case USER:
                BeanContext.lookup(LifeCycleTrigger.class)
                        .triggerLifeCycleEventOnUser(aInEntityIdModel.getUserId(), aInEventName);
                break;
            case GROUP:
                BeanContext.lookup(LifeCycleTrigger.class)
                        .triggerLifeCycleEventOnGroup(aInEntityIdModel.getGroupId(), aInEventName);
                break;
            default:
                TPAResources
                        .debug(logger, "Unsupported Entity type: [", aInEntityType, "] for triggering LifeCycleEvent");
        }
    }

    /**
     * This method is used to trigger SubscriptionExhaustionEvent on Account/Device/Subscription
     * based on ExhaustEntityType
     *
     * @param aInAccountId            Account Id on which event will be triggered
     * @param aInCounterInstanceClass Counter Instance object to get Subsciption id
     * @param aInEntityInformation    EntityInformation object to get Subscription Id
     * @param aInDeviceId             Device Id on which event will be triggered
     * @param aInExhaustEntityType    DEVICE/ACCOUNT/SUBSCRIPTION
     */
    private static void triggerSubscriptionExhaustionEvent(String aInAccountId,
            CounterInstanceClass aInCounterInstanceClass, EntityInformation aInEntityInformation,
            String aInDeviceId, ExhaustEntityType aInExhaustEntityType)
    {
        if(aInExhaustEntityType == null)
        {
            return;
        }
        switch (aInExhaustEntityType)
        {
            case SUBSCRIPTION:
                String lSubsId = null;
                if (aInEntityInformation != null)
                {
                    lSubsId = aInEntityInformation.getSubInstId();
                }
                else if (aInCounterInstanceClass != null)
                {
                    lSubsId = aInCounterInstanceClass.getSubsId();
                }
                TPAResources.debug(logger,
                        "Triggering subscription exhaustion event for subscriptionId: ", lSubsId);
                BeanContext.lookup(LifeCycleTrigger.class)
                        .triggerSubscriptionExhaustionEvent(lSubsId, aInExhaustEntityType);
                break;

            case DEVICE: ;
                TPAResources.debug(logger,
                        "Triggering subscription exhaustion event for deviceId: ", aInDeviceId);
                BeanContext.lookup(LifeCycleTrigger.class)
                        .triggerSubscriptionExhaustionEvent(aInDeviceId, aInExhaustEntityType);
                break;

            case ACCOUNT:
                TPAResources.debug(logger,
                        "Triggering subscription exhaustion event for accountId: ", aInAccountId);
                BeanContext.lookup(LifeCycleTrigger.class)
                        .triggerSubscriptionExhaustionEvent(aInAccountId, aInExhaustEntityType);
                break;
            default:
                TPAResources.debug(logger,
                        "Triggering subscription exhaustion event is not supported for ",
                        aInExhaustEntityType);
        }
    }

    /**
     * This Method trigger rule engine to fetch rule engine output to use it for further processing
     * related to threshold and notification
     *
     * @param aInThrshldPrfContext Threshold profile Context
     * @param aInEntityInformation Entity Information'
     * @param aInAccount Account for fetching users for notifications
     * @param aInThresholdInstanceMap Map containing threshold name as key and instance as value
     * @param aInThresholdProfile threshold profile definition from DB
     * @return ThresholdRuleResultWrapper List of Rule Engine result for further
     *         actions.
     */
    public static ThresholdRuleResultWrapper triggerRuleEngineForThreshold(
            ThresholdProfileContextImpl aInThrshldPrfContext, EntityInformation aInEntityInformation,
            Account aInAccount, NotificationCollectionImpl aInNotificationCollection,
            Map<String, ThresholdInstance> aInThresholdInstanceMap,
            ThresholdProfile aInThresholdProfile)
    {
        TPAResources.debug(logger, "triggerRuleEngineForThreshold Start");

        EntityCounterInstanceId lEntityCounterInstanceId = null;

        ThresholdRuleResultWrapper lThrRuleResultWrapper = new ThresholdRuleResultWrapper();

        SubscriptionContext lSubscriptionContext =
                EntityCounterUtility.getSubscriptionContext(aInEntityInformation);
        ChargingPolicyDecisionResult lThrResult = getRuleResultForThreshold(null,
                null, aInThrshldPrfContext, null, lThrRuleResultWrapper,
                lSubscriptionContext, null, null,
                null,null,null, null, new UserContextImpl ( ), null, null);
        if (lThrResult == null)
        {
            return lThrRuleResultWrapper;
        }

        processResultForThresholdContext(lThrResult, lThrRuleResultWrapper ,aInThrshldPrfContext);

        if (aInEntityInformation.getEntityType() == EntityTypeEnum.ENTITY_BUCKET)
        {
            // Mark intermediate thresholds crossed, while snr processing is not required
            ThresholdHandler.processIntermediateThresholdsAndGetSignalingState(
                    aInThresholdInstanceMap, aInThresholdProfile, lThrResult, false);
        }

        // Send Notification logic
        if (checkForSendNotification(aInThrshldPrfContext, RatingCallActions.COMMIT,
                ThresholdType.THRESHOLD_ON_COMMIT))
        {
            Set<String> lEntityIdSet = new HashSet<>();
            String lEntityType = null;
            if (aInAccount != null)
            {
                lEntityIdSet = getDeviceIdSetForAccount(aInAccount);
                lEntityType = Device.class.getSimpleName();
            }
            else
            {
                String lGroupId = getGroupId(null, aInEntityInformation,
                        null, lThrResult);
                String lEntityId = Objects.nonNull(lGroupId) ? lGroupId : null;
                lEntityType = Objects.nonNull(lGroupId)? Group.class.getSimpleName() : null;
                lEntityIdSet.add(lEntityId);
            }



        if (lEntityIdSet == null || lEntityIdSet.isEmpty())
        {
            TPAResources.debug(logger,
                    "RuleEngineHandler :: " , "EntityId Set is null");
            return lThrRuleResultWrapper;
        }

        processResultForNotifContext(aInNotificationCollection, lThrResult,
                        lThrRuleResultWrapper, lEntityType, lEntityIdSet, null,
                        aInThrshldPrfContext, null);
        }

        return lThrRuleResultWrapper;
    }

    /**
     * This method will parse the parameter of EXHAUST rule
     * attribute and will return the value as ExhaustEntityType enum.
     *
     * @param aInNotificationExhaust Generic Value Object of EXHAUST.
     * @return ExhaustEntityType
     */
    private static ExhaustEntityType getExhaustEntityType(
            List<GenericValueWrapper> aInNotificationExhaust)
    {
        if (aInNotificationExhaust == null)
        {
            return null;
        }
        ExhaustEntityType lExhaustEntityType = null;
        for (GenericValueWrapper lVariable : aInNotificationExhaust)
        {
            if(lVariable!=null)
            {
                GenericValue lGenericValue = lVariable.getValue();
                if (Objects.nonNull(lGenericValue))
                {
                    String lAction = lGenericValue.getValue();
                    if (lGenericValue.getType() == GenericValueType.ENUM)
                    {
                        lAction = StringUtils.substringAfter(lAction,
                                RatingConstants.DOT_STRING);
                        lExhaustEntityType = ExhaustEntityType.valueOf(lAction);
                        if (lExhaustEntityType != null)
                        {
                            break;
                        }
                    }
                }
            }
        }
        return lExhaustEntityType;
    }

    /**
     * method will try to get Account-Id and set in RatingAnswerResult
     *
     * @param aInREWrapper re wrapper
     * @param aInEntityInformation entity information
     * @param aInAccount account
     * @param aInEntityCounterInstance entitycounterinstance
     */
    private static void setAccountIdInRatingEngineAnswer(REWrapper aInREWrapper,
            EntityInformation aInEntityInformation, Account aInAccount,
            EntityCounterInstance aInEntityCounterInstance)
    {
        TPAResources.debug(logger, "entered in setAccountIdInRatingEngineAnswer");
        String lAccountId = null;
        if(aInAccount != null)
        {
            lAccountId = aInAccount.getId();
        }
        else if (aInREWrapper.getContextManagement() != null &&
                aInREWrapper.getContextManagement().getAccountId() != null)
        {
            lAccountId =  aInREWrapper.getContextManagement().getAccountId();
        }
        else if (aInEntityInformation != null &&
                aInEntityInformation.getAccountId() != null)
        {
            lAccountId = aInEntityInformation.getAccountId();
        }
        else if(aInEntityCounterInstance != null &&
                aInEntityCounterInstance.getAccount() != null &&
                aInEntityCounterInstance.getAccount().getId() != null )
        {
            lAccountId = aInEntityCounterInstance.getAccount().getId();
        }
        ((RatingEngineAnswerImpl)aInREWrapper.getRatingEngineRequest().getRatingEngineAnswer()).
            setAccountId(lAccountId);
        TPAResources.debug(logger, "exited from setAccountIdInRatingEngineAnswer");
    }

    /**
     * Set the counter instances into device context.
     *
     * @param aInDeviceContext         Device context
     * @param aInEntityCounterInstance Current updated entity counters
     */
    private static void updateCounterInfoInDeviceContext(
            DeviceContext aInDeviceContext,
            EntityCounterInstance aInEntityCounterInstance)
    {
        if (aInDeviceContext != null && aInEntityCounterInstance != null &&
                !CollectionUtils.isEmpty(
                        aInEntityCounterInstance.getCounterInstanceList()) &&
                aInEntityCounterInstance.getId()
                        .getEntityCounterInstanceAttachMode() ==
                        EntityCounterInstanceAttachMode.DEVICE)
        {
            aInEntityCounterInstance.getCounterInstanceList()
                    .forEach((aInCounterInstanceClass) -> {
                        aInDeviceContext.addDeviceCounters(
                                aInCounterInstanceClass.getCounterDefId(),
                                aInCounterInstanceClass);
                        long lCounterEndDate =
                                ThresholdExecutionUtility
                                        .evaluateEndDateForCounter(
                                                aInEntityCounterInstance,
                                                aInCounterInstanceClass);
                        aInDeviceContext.addDeviceCountersEndDate(
                                aInCounterInstanceClass.getCounterDefId(),
                                lCounterEndDate);

                    });
        }
    }

    /**
     * Notification logic will be executed in case of: 1. Commit call 2. In case
     * threshold with reservation flag high is attached with bucket. Currently
     * threshold on reservation can be attached with bucket entity only 3. If
     * the isSendNotificationAllowed flag is set in {@link
     * ThresholdProfileContextImpl}
     *
     * @param aInThrshldPrfContext threshold profile context
     * @param aInReserveCommit     Rating call action
     * @param aInThresholdType     threshold type
     * @return boolean value. If any of the condition is satisfied, send true.
     * Else send false.
     */
    private static boolean checkForSendNotification(
            ThresholdProfileContextImpl aInThrshldPrfContext,
            RatingCallActions aInReserveCommit, ThresholdType aInThresholdType)
    {
        return aInReserveCommit == RatingCallActions.COMMIT ||
                aInThresholdType == ThresholdType.THRESHOLD_ON_RESERVATION ||
                aInThrshldPrfContext.isSendNotificationAllowed();
    }

    /**
     * Add AVP's as specified in actions for threshold
     *
     * @param aInChargingPolicyDecisionResult Threshold execution result
     * @param aInGyMessageContext             Gy message context
     * @param aInDevice                       Device object for Notification
     * @param aInAccount                      Account Object
     * @param aInCDREngine                    CDREngine object
     * @param aInIsFUISetForMscc              flag for FUI set in MSCC
     * @param aInSubscriptionContext          Subscription context
     * @param aInRatingEngineRequest          Rating Engine request object
     */
    public static void addActionAVP(ChargingPolicyDecisionResult aInChargingPolicyDecisionResult,
            GyMessageContext aInGyMessageContext, Device aInDevice, Account aInAccount,
            CDREngine aInCDREngine, boolean aInIsFUISetForMscc, SubscriptionContext aInSubscriptionContext,
                                    RatingEngineRequest aInRatingEngineRequest)
    {
        if(!aInIsFUISetForMscc)
        {
            createFinalUnitIndication(aInChargingPolicyDecisionResult, aInGyMessageContext);
        }
        if (aInChargingPolicyDecisionResult.getContextResults(
                ResultContextType.CALL_RESULT) != null)
        {
            AVPUtil lAVPUtil = new AVPUtil();
            aInGyMessageContext.getAVPDetails().addAll(
                    lAVPUtil.getAVPs(aInChargingPolicyDecisionResult));
            if (!aInRatingEngineRequest.getExternalUsageData().isSuppressNotifAndAnn())
            {
                aInGyMessageContext.getCurrentMscc().
                        getAnnouncementInformations().addAll(lAVPUtil.getAnnouncementAVP(
                                aInChargingPolicyDecisionResult, aInDevice));
            }
            else
            {
                TPAResources.debug(logger, "The threshold announcement for trusted number calls has ",
                        "been suppressed for device id [", aInDevice != null ? aInDevice.getId():null, "]");
            }
            SSIDetail lSsiDetail = lAVPUtil.getSSIDetail(
                    aInChargingPolicyDecisionResult);
            if (CommonUtil.isNotNullOrEmpty(lSsiDetail))
            {
                TPAResources.debug(logger,
                        "Set SSIDetail AVP in Gy Message Context.");
                aInGyMessageContext.setsSIDetail(lSsiDetail);
            }
            lAVPUtil.addAVPToCDR(aInChargingPolicyDecisionResult, aInCDREngine);
            if(GlobalConfig.get5GDeviceIdentitiesFlagInReports() ==1)
            {
                EDRUtility.addAVPToEDR(aInChargingPolicyDecisionResult, aInDevice, aInSubscriptionContext);
            }else{
                EDRUtility.addAVPToEDR(aInChargingPolicyDecisionResult, aInAccount, aInSubscriptionContext);
            }
        }
    }
    /**
     * This method creates FinalIndicationUnit object and sets
     * it to current mscc of GyMessageContext
     *
     * @param aInChargingPolicyDecisionResult ChargingPolicyDecisionResult
     * @param aInGyMessageContext GyMessageContext
     */
    public static void createFinalUnitIndication(
            ChargingPolicyDecisionResult aInChargingPolicyDecisionResult,
            GyMessageContext aInGyMessageContext)
    {
        Long lOutGSUPresent = null;
        MultipleServicesCreditControl lMscc = aInGyMessageContext
                .getCurrentMscc();
        FinalUnitIndication lFUI = lMscc.getFinalUnitIndication() != null ?
                lMscc.getFinalUnitIndication() : new FinalUnitIndication();
        Long lValidityTime = getVTandUpdateFUI(aInChargingPolicyDecisionResult, lFUI);
        if (lValidityTime != null)
        {
            lMscc.setValidityTime(lValidityTime);
        }
        lMscc.setFinalUnitIndication(lFUI);

        lOutGSUPresent = aInChargingPolicyDecisionResult.getAttributeResult(ResultContextType.RATING,
                ChargingRuleAttributeName.GSU_VALUE);
        TPAResources.debug(logger, "GSU-value from threshold rule evaluated : ",
                lOutGSUPresent);
        if(lOutGSUPresent != null)
        {
            aInGyMessageContext.setGSUfromThresholdRule(lOutGSUPresent);
            TPAResources.debug(logger, "setGSUfromThresholdRule in aInGyMessageContext :",
                    lOutGSUPresent);
        }
    }

    /**
     * This method sets FinalIndicationUnit and returns the validity time to
     * be extended in GYSession/Mscc in case of FinalUnitAction
     * is set to REDIRECT
     *
     * @param aInChargingPolicyDecisionResult ChargingPolicyDecisionResult
     * @param aInOutFUI FinalUnitIndicationObject
     * @return Validity Time
     */
    public static Long getVTandUpdateFUI(
            ChargingPolicyDecisionResult aInChargingPolicyDecisionResult,
            FinalUnitIndication aInOutFUI)
    {
        TPAResources.debug(logger, "Looking for FUI AVP in Rules:",
                aInChargingPolicyDecisionResult);
        Long aOutValidityTime = null;
        GenericValueWrapper lFinalUnitActionWrapper = aInChargingPolicyDecisionResult
                .getAttributeResult(ResultContextType.RATING,
                        ChargingRuleAttributeName.FINAL_UNIT_ACTION_REDIRECT);
        Boolean lFuaTerminate = aInChargingPolicyDecisionResult
                .getAttributeResult(ResultContextType.RATING,
                        ChargingRuleAttributeName.FINAL_UNIT_ACTION_TERMINATE);
        Boolean lFinalUnitActionRestrictAccess = aInChargingPolicyDecisionResult
                .getAttributeResult(ResultContextType.RATING,
                        ChargingRuleAttributeName.FINAL_UNIT_ACTION_RESTRICT_ACCESS);
        Set<String> lFilterIds = getFilterIds(aInChargingPolicyDecisionResult);
        Set<String> lRestrictionFilterRules = getRestrictionFilterRules(aInChargingPolicyDecisionResult);
        if (lFuaTerminate != null)
        {
            updateFUIAVP(aInOutFUI, FinalUnitAction.TERMINATE, null, null,null);
        }
        else if (lFinalUnitActionWrapper != null)
        {
            GenericValueConverter lConverter = RuleEnginePluginCache
                    .getInstance(
                            ChargingPolicyConstants.CHARGING_PDF_APPLICATION)
                    .getConverter();
            String lRedirectServerAddress = lFinalUnitActionWrapper.getName();

            RedirectAddressType lRedirectAddressType = (RedirectAddressType)
                    (lConverter.extractFromGeneric(
                            lFinalUnitActionWrapper.getValues()[1], false));
            aOutValidityTime = ((BigInteger) (lConverter.extractFromGeneric(
                    lFinalUnitActionWrapper.getValues()[0], false)))
                    .longValue();
            RedirectServer lRedirectServer =
                    new RedirectServer(lRedirectAddressType, lRedirectServerAddress);
            updateFUIAVP(aInOutFUI, FinalUnitAction.REDIRECT,null, lFilterIds , lRedirectServer);
        }
        else if (Objects.nonNull(lFinalUnitActionRestrictAccess) && Objects.nonNull(lFilterIds))
        {
            updateFUIAVP(aInOutFUI, FinalUnitAction.RESTRICT_ACCESS, null,lFilterIds , null);
        }
        else if (Objects.nonNull(lFinalUnitActionRestrictAccess) && Objects.nonNull(lRestrictionFilterRules))
        {
            updateFUIAVP(aInOutFUI, FinalUnitAction.RESTRICT_ACCESS, lRestrictionFilterRules,null , null);
        }
        return aOutValidityTime;
    }

    /**
     * Method is used to Update FUI AVP data.
     *
     * @param aInOutFUI          : FinalUnitIndication
     * @param aInFinalUnitAction : FinalUnitAction
     * @param aInRestrictionFilterRules : RestrictionFilter Rules
     * @param aInFilterIds       : Filter Ids
     * @param aInRedirectServer  : RedirectServer
     */
    private static void updateFUIAVP(FinalUnitIndication aInOutFUI, FinalUnitAction aInFinalUnitAction,Set<String> aInRestrictionFilterRules,
            Set<String> aInFilterIds, RedirectServer aInRedirectServer)
    {
        TPAResources.debug(logger, "Update FUI AVP with FinalUnitAction:", aInFinalUnitAction,"RestrictionFilter Rules: ", aInRestrictionFilterRules,
                "Filter Ids: ", aInFilterIds);
        aInOutFUI.setFinalUnitAction(aInFinalUnitAction);
        aInOutFUI.setRestrictionFilterRules(aInRestrictionFilterRules);
        aInOutFUI.setFilterIds(aInFilterIds);
        aInOutFUI.setRedirectServer(aInRedirectServer);
    }

    /**
     * Method is used to fetch Filter Ids for FUI from Rule result
     *
     * @param aInChargingPolicyDecisionResult ChargingPolicyDecisionResult
     * @return Set of Filter IDs
     */
    private static Set<String> getFilterIds(
            ChargingPolicyDecisionResult aInChargingPolicyDecisionResult)
    {
        Set<String> lOutFilterIds = null;
        List<String> lFilterIds = aInChargingPolicyDecisionResult
                .getAttributeResult(ResultContextType.RATING,
                        ChargingRuleAttributeName.FINAL_UNIT_INDICATION_FILTER_ID);
        if (CommonUtil.isNullOrEmpty(lFilterIds))
        {
             TPAResources.debug(logger, "no filter Ids in rule result");
             return null;
        }
        lOutFilterIds = new HashSet<>();
        for (String lId : lFilterIds)
        {
            if (!lId.trim().isEmpty())
            {
            lOutFilterIds.add(lId.trim());
            }
        }
        TPAResources.debug(logger, "FUI Filter ID Found From Rules:", lOutFilterIds);
        return lOutFilterIds;
    }

    /**
     * Method is used to fetch Restriction Filter Rule for FUI from Rule result
     *
     * @param aInChargingPolicyDecisionResult ChargingPolicyDecisionResult
     * @return Set of Restriction Filter Rules
     */
    private static Set<String> getRestrictionFilterRules(
            ChargingPolicyDecisionResult aInChargingPolicyDecisionResult)
    {
        Set<String> lOutFilterRules = null;
        List<String> lFilterRules = aInChargingPolicyDecisionResult
                .getAttributeResult(ResultContextType.RATING,
                        ChargingRuleAttributeName.FINAL_UNIT_INDICATION_RESTRICTION_FILTER_RULE);
        if (CommonUtil.isNullOrEmpty(lFilterRules))
        {
             TPAResources.debug(logger, "no filter rules in rule result");
             return null;
        }
        lOutFilterRules = new HashSet<>();
        for (String lRule : lFilterRules)
        {
            if (!lRule.isEmpty())
            {
                lOutFilterRules.add(lRule);
            }
        }
        TPAResources.debug(logger, "filter rules:", lOutFilterRules);
        return lOutFilterRules;
    }

    /**
     * Method to set Trigger info in current mscc of GyMessageContext
     *
     * @param aInResult ChargingPolicyDecisionResult
     * @param aInGyMessageContext GyMessageContext
     */
    public static void createTriggerAvp(ChargingPolicyDecisionResult aInResult,
            GyMessageContext aInGyMessageContext)
    {
        if (aInResult == null ||
                (aInGyMessageContext == null || aInGyMessageContext.getCurrentMscc() == null))
        {
            TPAResources.debug(logger,
                    "Found null GyMessageContext or ChargingPolicyDecisionResult so returning.");
            return;
        }
        Trigger lTrigger = new Trigger();
        Set<TriggerType> lTriggerTypes = new HashSet<TriggerType>();
        ArrayList lTriggerContainer = new ArrayList();
        lTriggerContainer = aInResult
                .getAttributeResult(ResultContextType.CALL_RESULT,
                        ChargingRuleAttributeName.TRIGGER_TYPE);
        if (CollectionUtils.isNotEmpty(lTriggerContainer))
        {
            for (Object lArray : lTriggerContainer)
            {
                TriggerType lTriggerType = (TriggerType) lArray;
                lTriggerTypes.add(lTriggerType);
            }

            lTrigger.setTriggerType(lTriggerTypes);

            aInGyMessageContext.getCurrentMscc().setTrigger(lTrigger);
        }
    }

    /**
     * Method to create NchfTrigger object and set in current mscc of
     * GyMessageContext
     *
     * @param aInResult ChargingPolicyDecisionResult
     * @param aInGyMessageContext GyMessageContext
     */
    public static void createNchfTriggerAvp(ChargingPolicyDecisionResult aInResult,
            GyMessageContext aInGyMessageContext)
    {
        Map<String, Object> lCallResultMap = null;
        if (aInResult != null && aInResult.getResult() != null
                && !aInResult.getResult().isEmpty())
        {
            Map<String, Map<String, Object>> lResultMap = aInResult.getResult();
            lCallResultMap = lResultMap.get(ResultContextType.CALL_RESULT);
        }

        if (lCallResultMap != null && !lCallResultMap.isEmpty())
        {
            ArrayList<NCHFTrigger> lArmNchfTriggerList = (ArrayList<NCHFTrigger>) lCallResultMap
                    .get(ChargingRuleAttributeName.ARM_NCHF_TRIGGER.value());
            if (!CommonUtil.isNullOrEmpty(lArmNchfTriggerList))
            {
                aInGyMessageContext.getCurrentMscc()
                        .setNchfTriggers(lArmNchfTriggerList);
            }
        }
    }

    /**
     * Evaluate Rule for Threshold.
     *
     * @param aInCContext Common Call Context for input to rule engine.
     * @param aInDeviceContext Device context for condition on Threshold
     * @param aInThrshldPrfContext Threshold Profile Context for input to rule
     *            engine.
     * @param aInServiceInfoContext the service info context
     * @param aInOutThrRuleResultWrapper Threshold rule result wrapper for
     *            result in case of error.
     * @param aInSubscriptionContext Subscription Context
     * @param aInGyMessageContext GyMessage Context
     * @param aInImsServiceInformationContext IMS Service Info Context
     * @param aInNchfChargingMessageContext NCHF Charging Message Context
     * @param aInGroupContext {@link GroupContext}
     * @param aInECommerceCallContext ECommerce Call Context
     * @param aInAccountContext Account Context
     * @return Threshold result from rule engine
     */
    private static ChargingPolicyDecisionResult getRuleResultForThreshold(
            CommonCallContext aInCContext, DeviceContext aInDeviceContext,
            ThresholdProfileContextImpl aInThrshldPrfContext,
            ServiceInfoContext aInServiceInfoContext,
            ThresholdRuleResultWrapper aInOutThrRuleResultWrapper,
            SubscriptionContext aInSubscriptionContext,
            GyMessageContext aInGyMessageContext,
            ImsServiceInformationContext aInImsServiceInformationContext,
            NchfChargingMessageContext aInNchfChargingMessageContext,
            GroupContext aInGroupContext,
            ECommerceCallContext aInECommerceCallContext,
            AccountContext aInAccountContext,
            UserContext aInUserContext,
            EntityInformation aIEntityInformation,
            SessionParameterContext aInSessionParameterContext)
    {
        // Add common call context and threshold profile context for rule engine
        Map<String, SourceContext> lSourceContexts = new HashMap<>();
        lSourceContexts.put(SourceContextType.CALL_COMMON, aInCContext);
        lSourceContexts.put(SourceContextType.SPR_DEVICE, aInDeviceContext);
        lSourceContexts.put(SourceContextType.SPR_ACCOUNT, aInAccountContext);
        lSourceContexts.put(SourceContextType.THRESHOLD_PROFILE,
                aInThrshldPrfContext);
        lSourceContexts.put(SourceContextType.SERVICE_INFO,
                aInServiceInfoContext);
        lSourceContexts.put(SourceContextType.SUBSCRIPTION,
                aInSubscriptionContext);
        lSourceContexts.put(SourceContextType.GY_MESSAGE, aInGyMessageContext);
        lSourceContexts.put(SourceContextType.IMS_SERVICE_INFO,
                aInImsServiceInformationContext);
        lSourceContexts.put(SourceContextType.NCHF_CHARGING_MESSAGE,
                aInNchfChargingMessageContext);
        lSourceContexts.put(SourceContextType.SPR_GROUP,aInGroupContext);
        lSourceContexts.put ( SourceContextType.SPR_USER , aInUserContext );
        if (aInSessionParameterContext != null)
        {
            lSourceContexts.put(SourceContextType.SESSION_PARAMETERS, aInSessionParameterContext);
        }
        if (aInECommerceCallContext != null)
        {
            lSourceContexts.put(SourceContextType.ECOMMERCE, aInECommerceCallContext);
        }


        String lRuleTableName = aInThrshldPrfContext.getProfileRuleTableName();
        if (lRuleTableName == null)
        {
            TPAResources.error(logger,
                    "Rule table name for Threshold Profile is NULL: ",
                    "Return Threshold BYPASS");
            aInOutThrRuleResultWrapper
                    .setDefaultAction(RuleEngineEnum.THRESHOLD_BYPASS);
            return null;
        }

        // Load the tpg to be used for rules loading if needed
        ChargingPolicyDecisionResult lThrResult =
                new ChargingPolicyDecisionResultImpl();
        String lTPGName =
                aInThrshldPrfContext.getThresholdProfileGroupName();
        if (CommonUtil.isNotNullOrEmpty(
                lTPGName))
        {
            ThresholdProfileGroup lTpg = Services.lookup(
                    ThresholdProfileGroupService.class).read(lTPGName);
            lThrResult.setSourceEntity(lTpg);
        }

        // Evaluate Rule for Threshold
        ChargingPolicyDecisionWorker.evaluateRuleTable(
                "RuleTable:" + lRuleTableName, RuleSetType.THRESHOLD,
                lSourceContexts, lThrResult, aIEntityInformation);
        return lThrResult;
    }

    /**
     * Process actions for Threshold context in the result returned by rule
     * engine.
     *
     * @param aInThrRuleResult Result from rule engine.
     * @param aInOutThrRuleResultWrapper Processed result is stored in this
     *            wrapper.
     * @param aInThrshldPrfContext Threshold Profile Context for input to rule
     *            engine.
     */
    private static void processResultForThresholdContext(
            ChargingPolicyDecisionResult aInThrRuleResult,
            ThresholdRuleResultWrapper aInOutThrRuleResultWrapper,
            ThresholdProfileContextImpl aInThrshldPrfContext)
    {
        // Look for the Threshold Profile Count
        Integer lThresholdInterval = aInThrRuleResult.getAttributeResult(
                ResultContextType.THRESHOLD,
                ChargingRuleAttributeName.THRESHOLD_PROFILE_INTERVAL_COUNT);
        aInOutThrRuleResultWrapper.setThresholdInterval(lThresholdInterval);

        // Look for Threshold Crossed
        List<GenericValueWrapper> lThresholdCrossed = aInThrRuleResult
                .getAttributeResult(ResultContextType.THRESHOLD,
                        ChargingRuleAttributeName.THRESHOLD_CROSSED);
        if (lThresholdCrossed == null)
        {
            TPAResources.debug(logger,
                    "RuleEngineHandler :: "
                            , "processResultForThresholdContext : "
                            , "No thresholds crossed in Threshold Profile:"
                            , " BYPASS");
            aInOutThrRuleResultWrapper
                    .setDefaultAction(RuleEngineEnum.THRESHOLD_BYPASS);
            return;
        }

        List<ThresholdRuleEngineResult> lThresholdRuleEngineResultList = new ArrayList<>();
        GenericValueConverter converter = RuleEnginePluginCache
                .getInstance(ChargingPolicyConstants.CHARGING_PDF_APPLICATION)
                .getConverter();
        for (GenericValueWrapper lThreshold : lThresholdCrossed)
        {
            ThresholdRuleEngineResult lThresholdRuleEngineResult = new ThresholdRuleEngineResult();
            // Fetch Threshold Name
            String lThresholdName = lThreshold.getName();

            // Fetch Threshold Recurrence Count
            BigInteger lThresholdRecurrenceCount = (BigInteger) (converter
                    .extractFromGeneric(lThreshold.getValue()));

         // Fetch Threshold Notification Send Count
            Long lThresholdNotificationSendCount = aInThrshldPrfContext
                    .getThresholdNotifSendCount(lThresholdName);

         // Fetch last PS notification sent timestamp
            Long lTimestampOfDailyNotiSent = aInThrshldPrfContext
                    .getThresholdTimestampOfDailyNotiSent(lThresholdName);
            if (lThresholdNotificationSendCount == null)
            {
                lThresholdNotificationSendCount = 0L;
            }
            if (lTimestampOfDailyNotiSent == null)
            {
                lTimestampOfDailyNotiSent = 0L;
            }
            if (logger.isDebugEnabled())
            {
                TPAResources.debug(logger, "RuleEngineHandler :: " ,
                            "processResultForThresholdContext: " ,
                            "Threshold ", lThresholdName, " crossed with " ,
                            "Recurrence Count For Threshold ",
                    lThresholdRecurrenceCount.intValue(), " , Notification send count is"
                    , lThresholdNotificationSendCount, " , Time stamp Of Daily Noti Sent is"
                    , lTimestampOfDailyNotiSent);
            }

            lThresholdRuleEngineResult.setThresholdName(lThresholdName);
            lThresholdRuleEngineResult.setThresholdRecurrenceCount(
                    lThresholdRecurrenceCount.intValue());
            lThresholdRuleEngineResult.setNotifSendCount(
                    lThresholdNotificationSendCount);
            lThresholdRuleEngineResult.setTimestampOfDailyNotiSent(
                    lTimestampOfDailyNotiSent);
            lThresholdRuleEngineResultList.add(lThresholdRuleEngineResult);
            setThresholdDiscountsInResult(aInThrRuleResult, lThresholdRuleEngineResult,
                    aInThrshldPrfContext);
        }
        aInOutThrRuleResultWrapper.setThresholdRuleEngineResultList(
                lThresholdRuleEngineResultList);
    }

    /**
     * set threshold discounts in result for counter and bucket as of now.
     *
     * @param aInThrRuleResult           ChargingPolicyDecisionResult
     * @param lThresholdRuleEngineResult ThresholdRuleEngineResult
     * @param aInThrshldPrfContext       ThresholdProfileContextImpl
     */
    private static void setThresholdDiscountsInResult(ChargingPolicyDecisionResult aInThrRuleResult,
            ThresholdRuleEngineResult lThresholdRuleEngineResult,
            ThresholdProfileContextImpl aInThrshldPrfContext)
    {
        if (ThresholdEntityType.BUCKET == aInThrshldPrfContext.getThresholdEntityType() ||
                ThresholdEntityType.COUNTER == aInThrshldPrfContext.getThresholdEntityType())
        {
            Object lThresholdUsageDiscount =
                    aInThrRuleResult
                            .getAttributeResult(ResultContextType.RATING, ChargingRuleAttributeName.USAGE_DISCOUNT);
            if (lThresholdUsageDiscount != null)
            {
                lThresholdRuleEngineResult.setDiscount((BigDecimal) lThresholdUsageDiscount);
            }
        }
    }

    /**
     * Process actions for Notification context in the result returned by rule
     * engine.
     *
     * @param aInNotificationCollection Collection of Notifications in the
     *            Rating Engine Request
     * @param aInThrRuleResult Result from rule engine.
     * @param aInOutThrRuleResultWrapper Processed result is stored in this
     *            wrapper.
     * @param aInEntityIdSet Entity ID Set for which notification must be sent.
     * @param aInEntityType Entity Type for which notification must be sent.
     * @param aInREWrapper Rating Engine Request wrapper as input.
     * @param aInThrshldPrfContext Threshold Profile Context
     * @param aInCounterInstanceClass Counter instance object
     */
    private static void processResultForNotifContext(
            NotificationCollectionImpl aInNotificationCollection,
            ChargingPolicyDecisionResult aInThrRuleResult,
            ThresholdRuleResultWrapper aInOutThrRuleResultWrapper,
            String aInEntityType, Set<String> aInEntityIdSet,
            REWrapper aInREWrapper, ThresholdProfileContextImpl
                    aInThrshldPrfContext, CounterInstanceClass aInCounterInstanceClass)
    {
        // extract information for stop notification
        List<GenericValueWrapper> lNotificationStop = aInThrRuleResult
                .getAttributeResult(ResultContextType.NOTIFICATION,
                        ChargingRuleAttributeName.NOTIFICATION_STOP);
        // extract Information for send notification
        List<GenericValueWrapper> lNotificationSend = aInThrRuleResult
                .getAttributeResult(ResultContextType.NOTIFICATION,
                        ChargingRuleAttributeName.SEND_NOTIFICATION);
        List<GenericValueWrapper> lNotificationsReject = aInThrRuleResult
                .getAttributeResult(ResultContextType.NOTIFICATION,
                        ChargingRuleAttributeName.NOTIFICATION_REJECT);

        // Extract information for Send Rar
        String lThrSendRar = aInThrRuleResult.getAttributeResult(
                ResultContextType.NOTIFICATION, ChargingRuleAttributeName.SEND_RAR);

        List<GenericValueWrapper> lNotificationsCont = aInThrRuleResult
                .getAttributeResult(ResultContextType.NOTIFICATION,
                        ChargingRuleAttributeName.NOTIFICATION_CONTINUE);
        // Extract information for Reject
        String lReject = aInThrRuleResult.getAttributeResult(
                ResultContextType.NOTIFICATION, ChargingRuleAttributeName.REJECT);

        List<GenericValueWrapper> lNotifications = Collections.EMPTY_LIST;

        if (!StringUtils.isBlank(lReject))
        {
            TPAResources.debug(logger, "RuleEngineHandler :: "
                    , "processResultForAccountNotifContext: "
                    , "Threshold action is REJECT.");
            aInOutThrRuleResultWrapper.setActionResult(ThresholdRuleAction.REJECT);
            return;
        }
        else if (lNotificationsReject != null && !lNotificationsReject.isEmpty())
        {
            TPAResources.debug(logger, "RuleEngineHandler :: "
                    , "processResultForNotifContext: "
                    , "Threshold action is Notification_Reject.");
            aInOutThrRuleResultWrapper.setActionResult(ThresholdRuleAction.REJECT);
            lNotifications = lNotificationsReject;
        }
        else if (lNotificationStop != null && !lNotificationStop.isEmpty())
        {
            TPAResources.debug(logger, "RuleEngineHandler :: "
                    , "processResultForNotifContext: Threshold action is "
                    , "NOTIFICATION_STOP.");
            lNotifications = lNotificationStop;
            aInOutThrRuleResultWrapper.setActionResult(ThresholdRuleAction.STOP);
        }
        else if (lNotificationSend != null && !lNotificationSend.isEmpty())
        {
            TPAResources.debug(logger, "RuleEngineHandler :: "
                    , "processResultForNotifContext: Threshold action is "
                    , "SEND_NOTIFICATION.");
            lNotifications = lNotificationSend;
        }
        else if (lNotificationsCont !=null && !lNotificationsCont.isEmpty())
        {
            TPAResources.debug(logger, "RuleEngineHandler :: "
                    , "processResultForNotifContext: Threshold action is "
                    , "NOTIFICATION_CONTINUE.");
            lNotifications = lNotificationsCont;
        }
        else if (!StringUtils.isBlank(lThrSendRar) && "SendRARAll".equalsIgnoreCase(lThrSendRar))
        {
            TPAResources.debug(logger, "SendRARAll: processResultForCapNotifContext: "
                    , "Threshold action is Send RAR All.");
            aInOutThrRuleResultWrapper.setActionResult(ThresholdRuleAction.SENDRAR);
            setActiveSessionsForRatingEngineAnswerSendRARAll(aInREWrapper, aInCounterInstanceClass);
        }
        else if (!StringUtils.isBlank(lThrSendRar))
        {
            TPAResources.debug(logger, "RuleEngineHandler :: "
                    , "processResultForCapNotifContext: "
                    , "Threshold action is Send RAR.");
            aInOutThrRuleResultWrapper.setActionResult(ThresholdRuleAction.SENDRAR);
            setActiveSessionsForRatingEngineAnswer(aInREWrapper);
        }

        /*
         * true if Send_Notification action from rule result has high
         * priority.
         */
        boolean lSendNotifPresent = lNotificationSend != null
                && !lNotificationSend.isEmpty()
                && lNotificationStop == null
                && lNotificationsReject == null;
        // Extract information for Stop
        Boolean lIgnoreNotif = aInThrRuleResult.getAttributeResult(
                ResultContextType.NOTIFICATION, ChargingRuleAttributeName.IGNORE_NOTIFICATION);

        if (lIgnoreNotif != null && lIgnoreNotif)
        {
            lSendNotifPresent = false;
            lNotifications = Collections.EMPTY_LIST;
        }

        if (lSendNotifPresent)
        {
            ThresholdTriggerType lThresholdTriggerType =
                    (ThresholdTriggerType) aInThrRuleResult
                            .getAttributeResult(ResultContextType.THRESHOLD,
                                    ChargingRuleAttributeName.THRESHOLD_TRIGGER_TYPE);
            TPAResources.debug(logger, "ThresholdTriggerType configuration= ",
                    lThresholdTriggerType);
            if (lThresholdTriggerType != null &&
                    lThresholdTriggerType == ThresholdTriggerType.PROVISION)
            {
                TPAResources.debug(logger, "ThresholdTriggerType is not " ,
                        "configured to send notification");
                return;
            }
            /* this check is for all the 4 kinds of profile type.
            ABS, ABE, Percentage and Zero threhsold. */
            if (!isSendNotif(aInOutThrRuleResultWrapper,
                    aInThrshldPrfContext))
            {
                return;
            }
        }

        /* this check for notificaton sent count. */
        if(!isSendNotifControl(lNotificationsReject,
                lNotifications,
                aInOutThrRuleResultWrapper,
                aInThrshldPrfContext,
                aInREWrapper))
        {
            return;
        }

        List<GenericValueWrapper> lAssociatedEntitiesIds =
                aInThrRuleResult.getAttributeResult(ResultContextType.NOTIFICATION,
                        ChargingRuleAttributeName.NOTIFICATION_SELECTED_DEVICES);

        // Internal handle notifications
        handleNotifications(aInNotificationCollection, aInThrRuleResult,
                aInOutThrRuleResultWrapper, aInEntityType, aInEntityIdSet,
                lNotifications, lAssociatedEntitiesIds);
    }

    /**
     * This method is used to set all active session , Here all active rating groups session are
     * evaluated and stored from the associated entites.
     * If no {@link CounterInstanceClass} is provided, it falls back to the default
     *
     * @param aInREWrapper ReWrapper object
     * @param aInCounterInstanceClass  counter instance object
     */
    private static void setActiveSessionsForRatingEngineAnswerSendRARAll(
            REWrapper aInREWrapper, CounterInstanceClass aInCounterInstanceClass)
    {

        RatingEngineRequest lRatingEngineRequest = aInREWrapper.getRatingEngineRequest();
        ContextManagement lContextManagement = aInREWrapper.getContextManagement();
        if (aInCounterInstanceClass == null)
        {
            TPAResources.debug(logger, "SendRARAll: No CounterInstanceClass provided, ",
                    "falling back to default setActiveSessions method.");
            setActiveSessionsForRatingEngineAnswer(aInREWrapper);
            return;
        }
        RatingEngineAnswerImpl lRatingEngineAnswer = (RatingEngineAnswerImpl) lRatingEngineRequest.getRatingEngineAnswer();
        Set<String> lActiveSessions = lRatingEngineAnswer.getActiveSession();
        if (lActiveSessions == null)
        {
            lActiveSessions = new HashSet<>();
            lRatingEngineAnswer.setActiveSession(lActiveSessions);
        }
        lRatingEngineAnswer.setSendRARAll(true);
        Set<String> lActiveSessionsFromReserveMap = new HashSet<>();
        String lCounterInsId = aInCounterInstanceClass.getCounterInsId();
        if (lCounterInsId != null)
        {
            CounterInstanceClassPar lCounterInstanceClassPar = (CounterInstanceClassPar)
                    CacheThreadLocalVar.THREAD_LOCAL_VAR.getCounterInstanceClassParMap().get(lCounterInsId);
            if (lCounterInstanceClassPar != null)
            {
                Map<String, ?> lReserveMap = lCounterInstanceClassPar.getReserveMap();
                if (lReserveMap != null)
                {
                    lActiveSessionsFromReserveMap.addAll(lReserveMap.keySet());
                    TPAResources.debug(logger, "SendRARAll: CounterEntity:[",lCounterInstanceClassPar.getCounterInsId(),
                    "]reserveMap sessions added: ", lReserveMap.keySet());
                }
            }
        }
        Map<String, List<String>> lUCRefCtrInsIDs = lRatingEngineRequest.getExternalUsageData().getRefCounterInstanceIdMap();
        if (lUCRefCtrInsIDs != null)
        {
            processUCReserveMapSessions(aInCounterInstanceClass, lActiveSessionsFromReserveMap,
                    lUCRefCtrInsIDs);
        }
        lActiveSessions.addAll(lActiveSessionsFromReserveMap);
        lActiveSessions.add(lContextManagement.getSessionKey());
    }

    /**
     * This method retrieves a list of UC reference counter instance IDs from the provided reference map,
     * then iterates through each, accessing the corresponding {@link CounterInstanceClassPar} from the thread-local
     * cache
     * @param aInCounterInstanceClass   Counter Instance object
     * @param aInActiveSessionsFromReserveMap Active session reservation map
     * @param aInUCRefCtrInsIDs   UC reference counter
     */
    private static void processUCReserveMapSessions(CounterInstanceClass aInCounterInstanceClass,
            Set<String> aInActiveSessionsFromReserveMap,
            Map<String, List<String>> aInUCRefCtrInsIDs)
    {
        List<String> lUCRefCtrInsIDList = aInUCRefCtrInsIDs.get(aInCounterInstanceClass.getCounterDefId());
        if (lUCRefCtrInsIDList != null)
        {
            for (String lUCRefCtrInsID : lUCRefCtrInsIDList)
            {
                if (lUCRefCtrInsID == null) continue;
                CounterInstanceClassPar lCounterInstanceClassParUC = (CounterInstanceClassPar)
                        CacheThreadLocalVar.THREAD_LOCAL_VAR.getCounterInstanceClassParMap().get(lUCRefCtrInsID);
                if (lCounterInstanceClassParUC != null)
                {
                    Map<String, ?> lReserveMapUC = lCounterInstanceClassParUC.getReserveMap();
                    if (lReserveMapUC != null && lReserveMapUC.keySet() != null)
                    {
                        aInActiveSessionsFromReserveMap.addAll(lReserveMapUC.keySet());
                        TPAResources.debug(logger, "SendRARAll :: UCRefrenceCounter:[", lUCRefCtrInsID ,
                                "reserveMap sessions added: ", lReserveMapUC.keySet());
                    }
                }
            }
        }
    }

    /**
     * Internal handle actions for Notification context in the result
     * returned by rule engine.
     *
     * @param aInNotificationCollection Collection of Notifications in the
     *            Rating Engine Request
     * @param aInThrRuleResult Result from rule engine.
     * @param aInOutThrRuleResultWrapper Processed result is stored in this
     *            wrapper.
     * @param aInEntityIdSet Entity ID Set for which notification must be sent.
     * @param aInEntityType Entity Type for which notification must be sent.
     * @param aInNotifActions Notification actions.
     * @param aInAssociatedDeviceIds List of devices associated ids
     */
    private static void handleNotifications(
            NotificationCollectionImpl aInNotificationCollection,
            ChargingPolicyDecisionResult aInThrRuleResult,
            ThresholdRuleResultWrapper aInOutThrRuleResultWrapper,
            String aInEntityType, Set<String> aInEntityIdSet,
            List<GenericValueWrapper> aInNotifActions,
            List<GenericValueWrapper> aInAssociatedDeviceIds)
    {
        TPAResources.debug(logger, "RuleEngineHandler :: "
                , "processResultForNotifContext: Number of Notifications "
                , "actions ", aInNotifActions.size());
        if (aInNotifActions.isEmpty())
        {
            TPAResources.debug(logger, "threshold actions are not configured "
                    , "for any notification");
        }
        else
        {
            // initialize list of notifications
            Set<NotificationDetail> lNotificationSet = createSetOfNotifications(
                    aInNotifActions, false, null,
                    aInAssociatedDeviceIds);
            // process lNotifications for notification variables
            List<GenericValueWrapper> lNotificationVariables = aInThrRuleResult
                    .getAttributeResult(ResultContextType.NOTIFICATION,
                            ChargingRuleAttributeName.NOTIFICATION_VARIABLE);

            Map<String, String> lVarsMapTemp = aInThrRuleResult
                    .getContextResults(ResultContextType.NOTIFICATION_VARIABLES);
            Map<String, String> lVarsMap = new HashMap<>();
            for (String lKey : lVarsMapTemp.keySet())
            {
                String lValueStr = String.valueOf(lVarsMapTemp.get(lKey));
                lVarsMap.put(lKey, lValueStr);
            }

            handleSMPPSourceAndDestination(aInThrRuleResult, lVarsMap);
            for (String lEntityId : aInEntityIdSet)
            {
                processNotification(aInNotificationCollection,lNotificationSet,
                        lNotificationVariables,aInOutThrRuleResultWrapper,
                        lEntityId, aInEntityType,lVarsMap);
            }
        }
    }

    /**
     * This method will check if crossed threshold instance has its sendNotif
     * flag true then Send_Notification action is already executed for that
     * threshold instance and do not process it again. Threshold instance
     * sendNotif flag will be checked for absolute from end thresholds only
     * because the flag is introduced only to handle one notification sent for
     * absolute from end threshold.
     *
     * @param aInOutThrRuleResultWrapper    Processed result is stored in this
     *                                      wrapper.
     * @param aInThrshldPrfContext          Threshold profile context to which
     *                                      result wrapper belongs.
     * @return                              Send_Notifcation to be processed or
     *                                      not.
     */
    private static boolean isSendNotif(
            ThresholdRuleResultWrapper aInOutThrRuleResultWrapper,
            ThresholdProfileContextImpl aInThrshldPrfContext)
    {
        boolean lOutSendNotification = false;
        if (aInOutThrRuleResultWrapper
                .getThresholdRuleEngineResultList() != null)
        {
            List<ThresholdRuleEngineResult> lThrsRuleEngineResultList =
                    aInOutThrRuleResultWrapper
                    .getThresholdRuleEngineResultList();
            for (ThresholdRuleEngineResult lRuleEngineThresholdResult
                    : lThrsRuleEngineResultList)
            {
                boolean lThresholdSendNotification = aInThrshldPrfContext
                        .getThresholdSendNotification(
                                lRuleEngineThresholdResult
                                        .getThresholdName());
                if (lThresholdSendNotification)
                {
                    TPAResources.debug(logger, "Notification already ",
                            "sent during provisioning. So do not send Notification");
                    return lOutSendNotification;
                }
                else
                {
                    lRuleEngineThresholdResult.setSendNotif(true);
                    lOutSendNotification = true;
                }
                // Exit the loop since ThresholdRuleEngineResultList will
                // always contain a single Rule Engine Result
                break;
            }
        }
        return lOutSendNotification;
    }

    private static void handleSMPPSourceAndDestination(ChargingPolicyDecisionResult aInThrRuleResult,
                                                       Map<String, String> aInVarsMap)
    {
        List<GenericValueWrapper> lNotificationSmppSource = aInThrRuleResult
                .getAttributeResult(ResultContextType.NOTIFICATION,
                        ChargingRuleAttributeName.SMPP_SOURCE);
        List<GenericValueWrapper> lNotificationSmppDestination = aInThrRuleResult
                .getAttributeResult(ResultContextType.NOTIFICATION,
                        ChargingRuleAttributeName.SMPP_DESTINATION);
        if (lNotificationSmppSource != null)
        {
            String lSmppSourceNumber = lNotificationSmppSource.get(0).getName();
            aInVarsMap.put(SPSCommonConstants.SMS_ORIG_ID, lSmppSourceNumber);
        }
        if (lNotificationSmppDestination != null)
        {
            String lSmppDestinationNumber =
                    lNotificationSmppDestination.get(0).getName();
            aInVarsMap.put(SPSCommonConstants.SMS_DEST_ID,
                    lSmppDestinationNumber);
        }
    }

    /**
     * This method will check When Notification send count.
     * ONCE,  means only sent once per billing cycle period
     * DAILY,  /* means only sent once per day
     * TIMES, /* means only sent in allowed maximum times
     * ALWAYS /*means alawys sent per billing cycle period
     *
     * @param aInNotificationsReject Processed result is stored in this
     *                  wrapper.
     * @param aInNotifications Processed result is stored in this wrapper.
     * @param aInOutThrRuleResultWrapper aInOutThrRuleResultWrapper Processed
     *            result is stored in this wrapper.
     * @param aInThrshldPrfContext Threshold profile context to which
     *            result wrapper belongs.
     * @param aInREWrapper Rating Engine Request wrapper as input.
     * @return Send_Notifcation to be processed or not.
     */

    private static boolean isSendNotifControl(
            List<GenericValueWrapper> aInNotificationsReject,
            List<GenericValueWrapper> aInNotifications,
            ThresholdRuleResultWrapper aInOutThrRuleResultWrapper,
            ThresholdProfileContextImpl aInThrshldPrfContext,
            REWrapper aInREWrapper)
    {
        TPAResources.debug(logger, "Start to check notification sent control. ");
        TPAResources.debug(logger, "aInNotificationsReject is ", aInNotificationsReject);
        TPAResources.debug(logger, "aInNotifications is ", aInNotifications);


        boolean lIsNotifications =
                aInNotificationsReject != null &&
                        !aInNotificationsReject.isEmpty() &&
                        !aInNotifications.isEmpty();
        boolean lIsNotificationReject =
                lIsNotifications &&
                        aInNotifications.iterator().next().getValues() != null &&
                        aInNotifications.iterator().next().getValues()[1] != null;
        boolean lIsOutThrRuleResultWrapper =
                lIsNotificationReject &&
                        aInOutThrRuleResultWrapper != null &&
                        aInOutThrRuleResultWrapper.getThresholdRuleEngineResultList() != null &&
                        !aInOutThrRuleResultWrapper.getThresholdRuleEngineResultList().isEmpty();

        if (lIsOutThrRuleResultWrapper)
        {
            GenericValueConverter lConverter = RuleEnginePluginCache.getInstance(
                            ChargingPolicyConstants.CHARGING_PDF_APPLICATION).
                                getConverter();
            NotificationSentControl lNotificationSentControl =
                    (NotificationSentControl) (lConverter.extractFromGeneric(
                            aInNotifications.iterator().next().getValue(1), false));
            Long lNotifSendCount = aInOutThrRuleResultWrapper.
                    getThresholdRuleEngineResultList().iterator().next().getNotifSendCount();
            if (lNotifSendCount == null)
            {
                lNotifSendCount = 0L;
            }
            TPAResources.debug(logger, "Notification Sent Control in the rule is ",
                    lNotificationSentControl);
            TPAResources.debug(logger, "Notif Send Count is ",
                    lNotifSendCount);
            if (lNotificationSentControl == NotificationSentControl.ONCE)
            {
                if (lNotifSendCount.longValue() > 0)
                {
                    return false;
                }
                aInOutThrRuleResultWrapper.getThresholdRuleEngineResultList().iterator().next(
                        ).setNotifSendCount(lNotifSendCount + 1);
            }
            else if (lNotificationSentControl == NotificationSentControl.TIMES)
            {
                if (aInNotifications.iterator().next().getValues()[2] != null)
                {
                    BigInteger lNotificationSentLimitbiginter =
                            (BigInteger) (lConverter.extractFromGeneric(
                                    aInNotifications.iterator().next().getValue(2), false));
                    long lNotificationSentLimit = lNotificationSentLimitbiginter.longValueExact();
                    TPAResources.debug(logger, "Notification Sent Limit in the rule is ",
                            lNotificationSentLimit);
                    if (lNotifSendCount.longValue() >= lNotificationSentLimit)
                    {
                        return false;
                    }
                    else
                    {
                        aInOutThrRuleResultWrapper.getThresholdRuleEngineResultList(
                                ).iterator().next().setNotifSendCount(lNotifSendCount + 1);
                    }
                }
                else
                {
                    TPAResources.error(logger, "Rule configuration error: When Notification"
                            , "Control Sent is configured as TIMES, Notification Sent Limit"
                            , "should also be configured.");
                    return false;
                }
            }
            else if (lNotificationSentControl == NotificationSentControl.DAILY)
            {
                if (isSameDayOfMillis(aInThrshldPrfContext,
                        aInOutThrRuleResultWrapper,
                        aInREWrapper))
                {
                    TPAResources.debug(logger, "Daily type, this is the same day, not "
                            , "send notificaiton. ");
                    return false;
                }
                else
                {
                    aInOutThrRuleResultWrapper.getThresholdRuleEngineResultList(
                            ).iterator().next().setTimestampOfDailyNotiSent(
                                    System.currentTimeMillis());
                }
            }
            else
            {
                TPAResources.debug(logger, "Always type, send the notification. ");
                aInOutThrRuleResultWrapper.getThresholdRuleEngineResultList(
                        ).iterator().next().setNotifSendCount(lNotifSendCount + 1);
            }
        }
        return true;
    }


    /**
     * This method will check current call timestamp is same with Timestamp Of
     *     Daily NotiSent when NotificationSentControl is Once
     *
     * @param aInThrshldPrfContext Threshold profile context to which
     *            result wrapper belongs.
     * @param aInOutThrRuleResultWrapper aInOutThrRuleResultWrapper Processed
     *            result is stored in this wrapper.
     * @param aInREWrapper Rating Engine Request wrapper as input.
     * @return is the same day between current call timestamp and Timestamp Of
     *            Daily NotiSent.
     */

    private static boolean isSameDayOfMillis(
            ThresholdProfileContextImpl aInThrshldPrfContext,
            ThresholdRuleResultWrapper aInOutThrRuleResultWrapper,
            REWrapper aInREWrapper)
    {
     // Get the timezone
        String lTimeZoneId = TimeUtility.getTimeZone().getID();
        if (aInREWrapper.getRatingEngineRequest() != null &&
                aInREWrapper.getRatingEngineRequest().getRatingEngineAnswer() != null
                && aInREWrapper.getRatingEngineRequest().
                    getRatingEngineAnswer().getAccountId() != null)
        {
            String laccountid = aInREWrapper.getRatingEngineRequest().
                    getRatingEngineAnswer().getAccountId();
            Account lAccount = Services.lookup(AccountService.class)
                    .read(laccountid);
            if (lAccount != null)
            {
                lTimeZoneId = lAccount.getTimeZoneId();
                TPAResources.debug(logger, "Account name is", lAccount);
                TPAResources.debug(logger, "Time Zone  is  ",
                        lTimeZoneId);
            }
        }
        TPAResources.debug(logger, "the account time zone is ",lTimeZoneId);
        List<ThresholdRuleEngineResult> lThrsRuleEngineResultList =
                aInOutThrRuleResultWrapper
                .getThresholdRuleEngineResultList();
        for (ThresholdRuleEngineResult lRuleEngineThresholdResult
                : lThrsRuleEngineResultList)
        {
            Long lTimestampOfDailyNotiSent = aInThrshldPrfContext
                    .getThresholdTimestampOfDailyNotiSent(
                            lRuleEngineThresholdResult
                                    .getThresholdName());
            if (lTimestampOfDailyNotiSent == null)
            {
                lTimestampOfDailyNotiSent = 0L;
            }
                TPAResources.debug(logger, "Timestamp Of Daily Noti Sent is :",
                        lTimestampOfDailyNotiSent);
            if (lTimestampOfDailyNotiSent.longValue() == 0)
            {
                TPAResources.debug(logger, "Timestamp Of Daily Noti Sent is null");
                return false;
            }

            ZonedDateTime lTranTimestampOfDailyNotiSent =
                    getReqZonedDateTime(lTimeZoneId, lTimestampOfDailyNotiSent.longValue());
            long lcurrenttime = System.currentTimeMillis();
            ZonedDateTime lTrancurrentTimestamp =
                    getReqZonedDateTime(lTimeZoneId, lcurrenttime);
            int lDaytimestampOfDailyNotiSent = lTranTimestampOfDailyNotiSent.getDayOfYear();
            int lDayCurrent = lTrancurrentTimestamp.getDayOfYear();
            TPAResources.debug(logger, "The day of Timestamp Of Daily Noti Sent is ",
                    lDaytimestampOfDailyNotiSent);
            TPAResources.debug(logger, "The day of current time is ",
                    lDayCurrent);
            if (lDaytimestampOfDailyNotiSent == lDayCurrent)
            {
                TPAResources.debug(logger, "TimestampOfDailyNotiSent and current time are",
                        " same day, not send notification");
                return true;
            }
            else
            {
                TPAResources.debug(logger, "TimestampOfDailyNotiSent and current time are",
                        " different day, send notification");
                return false;
            }
        }
        return true;
    }

    /**
     * Process actions for cap Notification context in the result returned by
     * rule engine.
     *
     * @param aInNotificationCollection Collection of Notifications in the
     *            Rating Engine Request
     * @param aInThrRuleResult Result from rule engine.
     * @param aInOutThrRuleResultWrapper Processed result is stored in this
     *            wrapper.
     * @param aInEntityId Entity ID for which notification must be sent.
     * @param aInEntityType Entity Type for which notification must be sent.
     * @param aInREWrapper Rating Engine Request wrapper as input.
     * @param aInThrshldPrfContext Threshold Profile Context
     *
     */
    private static void processResultForCapNotifContext(
            NotificationCollectionImpl aInNotificationCollection,
            ChargingPolicyDecisionResult aInThrRuleResult,
            ThresholdRuleResultWrapper aInOutThrRuleResultWrapper,
            String aInEntityId, String aInEntityType, REWrapper aInREWrapper,
            ThresholdProfileContextImpl aInThrshldPrfContext)
    {
        ThresholdRuleAction lActionResult = ThresholdRuleAction.CONTINUE;
        // Extract information for Send Rar
        String lCapSendRar = aInThrRuleResult.getAttributeResult(
                ResultContextType.NOTIFICATION,
                ChargingRuleAttributeName.SEND_RAR);
        // Extract Information for Notifications
        List<GenericValueWrapper> lNotificationsCont = aInThrRuleResult
                .getAttributeResult(ResultContextType.NOTIFICATION,
                        ChargingRuleAttributeName.NOTIFICATION_CONTINUE);
        List<GenericValueWrapper> lNotificationsStop = aInThrRuleResult
                .getAttributeResult(ResultContextType.NOTIFICATION,
                        ChargingRuleAttributeName.NOTIFICATION_STOP);
        // Extract information for Notification Reject
        List<GenericValueWrapper> lNotificationReject = aInThrRuleResult
                .getAttributeResult(ResultContextType.NOTIFICATION,
                        ChargingRuleAttributeName.NOTIFICATION_REJECT);
        // Extract information for Reject
        String lReject = aInThrRuleResult.getAttributeResult(
                ResultContextType.NOTIFICATION, ChargingRuleAttributeName.REJECT);
        List<GenericValueWrapper> lNotifications = Collections.EMPTY_LIST;
        if (!StringUtils.isBlank(lReject))
        {
            TPAResources.debug(logger, "RuleEngineHandler :: "
                    , "processResultForCapNotifContext: "
                    , "Cap action is REJECT.");
            lActionResult = ThresholdRuleAction.REJECT;
            aInOutThrRuleResultWrapper.setActionResult(lActionResult);
            return;
        }
        else if (lNotificationReject != null && !lNotificationReject.isEmpty())
        {
            TPAResources.debug(logger, "RuleEngineHandler :: "
                    , "processResultForCapNotifContext: "
                    , "Cap action is NOTIFICATION_REJECT.");
            lActionResult = ThresholdRuleAction.REJECT;
            lNotifications = lNotificationReject;
        }
        else if (!StringUtils.isBlank(lCapSendRar))
        {
            TPAResources.debug(logger, "RuleEngineHandler :: "
                    , "processResultForCapNotifContext: "
                    , "Cap action is Send RAR.");
            lActionResult = ThresholdRuleAction.SENDRAR;
            aInOutThrRuleResultWrapper.setActionResult(lActionResult);

            setActiveSessionsForRatingEngineAnswer(aInREWrapper);
            return;
        }
        else if (lNotificationsStop != null && !lNotificationsStop.isEmpty())
        {
            TPAResources.debug(logger, "RuleEngineHandler :: "
                    , "processResultForCapNotifContext: "
                    , "Cap action is NOTIFICATION_STOP.");
            lActionResult = ThresholdRuleAction.STOP;
            lNotifications = lNotificationsStop;
        }
        else if (lNotificationsCont != null && !lNotificationsCont.isEmpty())
        {
            TPAResources.debug(logger, "RuleEngineHandler :: "
                    , "processResultForCapNotifContext: "
                    , "Cap action is NOTIFICATION_CONTINUE.");
            lNotifications = lNotificationsCont;
        }
        TPAResources.debug(logger, "RuleEngineHandler :: "
                , "processResultForCapNotifContext: Number of Notifications "
                , "actions ", lNotifications.size());
        if (lNotifications.isEmpty())
        {
            TPAResources.debug(logger,
                    "cap actions are not configured " , "for any notification");
            return;
        }

        /* this check for notificaton sent count. */
        if(!isSendNotifControl(lNotificationReject,
                lNotifications,
                aInOutThrRuleResultWrapper,
                aInThrshldPrfContext,
                aInREWrapper))
        {
            return;
        }

        List<GenericValueWrapper> lAssociatedEntitiesIds =
                aInThrRuleResult.getAttributeResult(ResultContextType.NOTIFICATION,
                        ChargingRuleAttributeName.NOTIFICATION_SELECTED_DEVICES);

        // initialize list of notifications
        Set<NotificationDetail> lNotificationSet = createSetOfNotifications(
                lNotifications, false, null,
                lAssociatedEntitiesIds);
        // process lNotifications for notification varaibles
        List<GenericValueWrapper> lNotificationVariables = aInThrRuleResult
                .getAttributeResult(ResultContextType.NOTIFICATION,
                        ChargingRuleAttributeName.NOTIFICATION_VARIABLE);
        processNotification(aInNotificationCollection, lNotificationSet,
                lNotificationVariables, aInOutThrRuleResultWrapper, aInEntityId,
                aInEntityType, null);
        aInOutThrRuleResultWrapper.setActionResult(lActionResult);
    }

    /**
     * Internal function to set active session keys for RatingEngineAnswer.
     * @param aInREWrapper Rating Engine Request wrapper as input.
     */
    private static void setActiveSessionsForRatingEngineAnswer(
            REWrapper aInREWrapper)
    {
        RatingEngineRequest lRatingEngineRequest = aInREWrapper
                .getRatingEngineRequest();
        ContextManagement lContextManagement = aInREWrapper
                .getContextManagement();
        Set<String> lActiveSessions = lRatingEngineRequest
                .getRatingEngineAnswer().getActiveSession();
        if (lActiveSessions == null)
        {
            Set<String> lNewActiveSessions = new HashSet<String>();
            lNewActiveSessions.add(lContextManagement.getSessionKey());
            ((RatingEngineAnswerImpl) lRatingEngineRequest
                    .getRatingEngineAnswer())
                        .setActiveSession(lNewActiveSessions);
        }
        else
        {
            lActiveSessions.add(lContextManagement.getSessionKey());
        }
    }

    /**
     * This method will process all notifications to be sent for corresponding
     * call
     *
     * @param aInNotificationCollection {@link NotificationCollectionImpl}
     * @param aInNotificationSet Set of {@link NotificationDetail}
     * @param aInNotificationVariables List of {@link GenericValueWrapper}
     * @param aInOutThrRuleResultWrapper {@link ThresholdRuleResultWrapper}
     * @param aInEntityId Entity ID for which notification must be sent.
     * @param aInEntityType Entity Type for which notification must be sent.
     * @param aInVarsMap variable Map as identified from Notification_Vars
     *            result context and this applies to all templates
     */
    private static void processNotification(
            NotificationCollectionImpl aInNotificationCollection,
            Set<NotificationDetail> aInNotificationSet,
            List<GenericValueWrapper> aInNotificationVariables,
            ThresholdRuleResultWrapper aInOutThrRuleResultWrapper,
            String aInEntityId, String aInEntityType,
            Map<String, String> aInVarsMap)
    {
        // Removing club id from Entity id while sending notification
        String lEntityId = ClubbingUtility.getEntityBusinessId(aInEntityId);
        List<Object> lThrRuleRsltWrapperNotifList =
                aInOutThrRuleResultWrapper.getNotificationObjList();
        if (aInNotificationVariables == null
                || aInNotificationVariables.isEmpty())
        {
            TPAResources.debug(logger, "RuleEngineHandler :: "
                    , "processResultForNotifContext: No variables to be "
                    , "processed.");
            for (NotificationDetail lNotificationDetail : aInNotificationSet)
            {
                String lTemplateId = lNotificationDetail
                        .getNotificationTemplateId();
                MapBackedNotificationTemplateParameters lNotifParameters = null;
                if (aInVarsMap != null)
                {
                    lNotifParameters = new MapBackedNotificationTemplateParameters();
                    lNotifParameters.putAll(aInVarsMap);
                    lNotificationDetail.setVariableMap(aInVarsMap);
                }

                AssociatedEntities lAssociatedEntities =
                        NotificationHandler.createAssociatedEntities(
                                lNotificationDetail.getAssociatedEntitiesIds());

                lThrRuleRsltWrapperNotifList.add(aInNotificationCollection.createNotificationObj(
                        lEntityId, aInEntityType, lTemplateId, lAssociatedEntities,
                        lNotifParameters));
            }
        }
        else
        {
            TPAResources.debug(logger,
                    "RuleEngineHandler :: "
                            , "processResultForNotifContext :  Number of "
                            , "Notification Variables = ",
                    aInNotificationVariables.size());
            Map<String, Map<String, String>> lTemplate2VariableMap = processNotificationVariables(
                    aInNotificationVariables);
            addNotifVariablesToTemplates(lTemplate2VariableMap,
                    aInNotificationSet, aInNotificationCollection, lEntityId,
                    aInEntityType, aInVarsMap, lThrRuleRsltWrapperNotifList);
        }

        // TODO temporary method to print notification to logs
        // can be removed on integration with notification server
        printNotificationDetailsInLog(aInNotificationSet);
        if (aInOutThrRuleResultWrapper != null)
        {
            aInOutThrRuleResultWrapper
                    .setNotificationDetail(aInNotificationSet);
        }
    }

    /**
     * Traverse all variables and create a mapping of variables for the
     * different template IDs.
     *
     * @param aInNotificationVariables List of variables to be processed.
     * @return Map which stores a Map of variables for each Template ID.
     */
    private static Map<String, Map<String, String>> processNotificationVariables(
            List<GenericValueWrapper> aInNotificationVariables)
    {
        Map<String, Map<String, String>> lTemplate2VariableMap = new HashMap<>();
        for (GenericValueWrapper lVariable : aInNotificationVariables)
        {
            // fetch notification variables
            // name of variable in which value to be replaced
            String lVariableName = lVariable.getName();
            // Fetch value of variable
            String lVariableValue = lVariable.getValue(0) == null ? null
                    : lVariable.getValue(0).getValue();
            lVariableValue = NotificationHandler.removeEnumDataType(lVariable, lVariableValue);
            // Fetch template name
            String lTemplateName = lVariable.getValue(1) == null ? null
                    : lVariable.getValue(1).getValue();
            TPAResources.debug(logger, "RuleEngineHandler :: "
                    , "processNotificationVariables : Processing Notification "
                    , "Variable:VariableName-> ", lVariableName,
                    " " , "VariableValue->", lVariableValue,
                    ", " , "TemplateName->", lTemplateName);

            if ((lTemplateName == null) || (lTemplateName.isEmpty()))
            {
                Map<String, String> lCommonVariables = lTemplate2VariableMap
                        .get(null);
                if (lCommonVariables == null)
                {
                    lCommonVariables = new HashMap<>();
                    lTemplate2VariableMap.put(null, lCommonVariables);
                    lCommonVariables.put(lVariableName, lVariableValue);
                }
                else
                {
                    if (!lCommonVariables.containsKey(lVariableName))
                    {
                        lCommonVariables.put(lVariableName, lVariableValue);
                    }
                }
            }
            else
            {
                Map<String, String> lTemplateVars = lTemplate2VariableMap
                        .get(lTemplateName);
                if (lTemplateVars == null)
                {
                    lTemplateVars = new HashMap<>();
                    lTemplate2VariableMap.put(lTemplateName, lTemplateVars);
                    lTemplateVars.put(lVariableName, lVariableValue);
                }
                else
                {
                    if (!lTemplateVars.containsKey(lVariableName))
                    {
                        lTemplateVars.put(lVariableName, lVariableValue);
                    }
                }
            }
        }
        return lTemplate2VariableMap;
    }


    /**
     * Add the list of variables for each Template ID. This includes variables
     * specific to the Template ID as well as variables applicable to all
     * templates. Add the result for each Notification to the
     * NotificationCollection.
     *
     * @param aInTemplate2VariableMap Map of variables for each template. null
     *            key stores values applicable to all templates.
     * @param aInNotificationSet Set of notifications to be sent processed for
     *            variables.
     * @param aInNotificationCollection Collection of Notifications in the
     *            Rating Engine Request
     * @param aInEntityId Entity ID for which notification must be sent.
     * @param aInEntityType Entity Type for which notification must be sent.
     * @param aInVarsMap variable Map as identified from Notification_Vars
     *            result context and this applies to all templates
     * @param aInThrRulResultWrapperNotifList Notification List in RERuleResult Wrapper object
     */
    private static void addNotifVariablesToTemplates(
            Map<String, Map<String, String>> aInTemplate2VariableMap,
            Set<NotificationDetail> aInNotificationSet,
            NotificationCollectionImpl aInNotificationCollection,
            String aInEntityId, String aInEntityType,
            Map<String, String> aInVarsMap, List<Object> aInThrRulResultWrapperNotifList)
    {
        for (NotificationDetail lNotificationDetail : aInNotificationSet)
        {
            String lTemplateId = lNotificationDetail
                    .getNotificationTemplateId();
            /*
             * null key stores Map of variables which are applicable to all
             * notifications (say commonVariables). Add commonVariables first
             * the variables corresponding to template name so that template
             * specific vaiables have a precedence over commonVariables.
             */
            Map<String, String> lVariableMap = lNotificationDetail
                    .getVariableMap();
            Map<String, String> lListOfCommonVars = aInTemplate2VariableMap
                    .get(null);
            if (lListOfCommonVars != null)
            {
                lVariableMap.putAll(lListOfCommonVars);
            }
            if (aInVarsMap != null)
            {
                lVariableMap.putAll(aInVarsMap);
            }
            Map<String, String> lListOfVarsForTemplate = aInTemplate2VariableMap
                    .get(lTemplateId);
            if (lListOfVarsForTemplate != null)
            {
                lVariableMap.putAll(lListOfVarsForTemplate);
            }
            MapBackedNotificationTemplateParameters lNotifParameters =
                    new MapBackedNotificationTemplateParameters();
            Map<String, String> lNotificationVarMap = lNotifParameters
                    .getAsMap();
            lNotificationVarMap.putAll(lVariableMap);

            AssociatedEntities lAssociatedEntities =
                    NotificationHandler.createAssociatedEntities(
                            lNotificationDetail.getAssociatedEntitiesIds());

            aInThrRulResultWrapperNotifList.add(aInNotificationCollection
                    .createNotificationObj(aInEntityId, aInEntityType, lTemplateId,
                            lAssociatedEntities, lNotifParameters));
        }
    }

    /**
     * Process rule result to create a set of NotificationDetail objects.
     *
     * @param aInNotifications    List of notifications from rule engine.
     * @param aInIgnoreSameTempId Ignore same templates for sending
     *                            notifications
     * @param aInNotifTempIds     Notification temp ids from collection using which
     *                            notifications would be sent
     * @param aInAssociatedDeviceIds list of ids of associated devices
     * @return Set of NotificationDetail containing all details for each
     *         notification except for notification variables.
     */
    private static Set<NotificationDetail> createSetOfNotifications(
            List<GenericValueWrapper> aInNotifications,
            boolean aInIgnoreSameTempId , List<String> aInNotifTempIds,
            List<GenericValueWrapper> aInAssociatedDeviceIds)
    {
        Set<NotificationDetail> lNotificationSet = new HashSet<>();
        GenericValueConverter lConverter = RuleEnginePluginCache
                .getInstance(ChargingPolicyConstants.CHARGING_PDF_APPLICATION)
                .getConverter();
        List<SpecificAssociatedEntities> lAssociatedEntitiesList = getAssociatedEntitiesFromGVW(aInAssociatedDeviceIds, lConverter);
        for (GenericValueWrapper lNotificationWrapper : aInNotifications)
        {
            NotificationDetail lNotificationDetails = new NotificationDetail();
            // Notification Template Id
            String lNotificationTemplateId = lNotificationWrapper.getName();
            if (aInIgnoreSameTempId && aInNotifTempIds != null &&
                    aInNotifTempIds.contains(lNotificationTemplateId))
            {
                continue;
            }
            lNotificationDetails
                    .setNotificationTemplateId(lNotificationTemplateId);
            // Notification policy
            NotificationPolicy lPolicy;
            if (lNotificationWrapper.getValues() != null)
            {
                lPolicy= (NotificationPolicy) (lConverter
                        .extractFromGeneric(lNotificationWrapper.getValues()[0],
                                false));
            }
            else
            {
                lPolicy= (NotificationPolicy) (lConverter
                        .extractFromGeneric(lNotificationWrapper.getValue(),
                                false));
            }
            lNotificationDetails.setNotificationPolicy(lPolicy);

            lNotificationDetails.setAssociatedEntitiesIds(
                    lAssociatedEntitiesList);

            TPAResources.debug(logger, "RuleEngineHandler :: "
                    , "createSetOfNotifications : NotificationTemplateId ",
                    lNotificationTemplateId, ", NotificationPolicy-> ",
                    lPolicy);
            lNotificationSet.add(lNotificationDetails);
        }
        return lNotificationSet;
    }

    /**
     * print Notification Details In Logs .
     *
     * @param aInNotificationList list of {@link NotificationDetail}
     */
    private static void printNotificationDetailsInLog(
            Set<NotificationDetail> aInNotificationList)
    {
        TPAResources.debug(logger,
                "RuleEngineHandler :: ",
                "printNotificationDetailsInLog : Result for ",
                "Threshold: List of Notifications: ");
        if(logger.isDebugEnabled())
        {
            for (NotificationDetail lNotificationDetail : aInNotificationList)
            {
                TPAResources.debug(logger, lNotificationDetail.toString());
            }
        }
    }

    /**
     * Method to check if thesubscription is in final state.
     * @param aInSubscriptionInstId subscription id
     * @param aInCounterInstanceClass counterinstanceclass instance
     * @return
     */
    private static boolean isSubscriptionExpired(String aInSubscriptionInstId, CounterInstanceClass aInCounterInstanceClass)
    {
        if (aInSubscriptionInstId == null)
        {
            aInSubscriptionInstId = aInCounterInstanceClass.getSubsId();
        }
        if (aInSubscriptionInstId != null)
        {
            Subscription lSubscription = getSubscription(aInSubscriptionInstId);
            if (lSubscription != null)
            {
                if (lSubscription.getState(LifeCycleType.ENTITY) != null &&
                        lSubscription.getState(LifeCycleType.ENTITY).isFinalFlag())
                {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Method to fill SyResultAnswerInfo
     *
     * @param aInRatingEngineRequest RatingEngineRequest
     * @param aInCtrSNRState SNR state for the passed counter
     * @param aInCounterInstanceClass CounterInstanceClass
     * @param aInEntityCounterInstanceId EntityCounterInstanceId
     * @param aInFirstUseCounter true if FirstUse else false
     * @param aInSubscriptionInstId subscription id
     */
    public static void fillSyResultAnswerInfo(
            RatingEngineRequest aInRatingEngineRequest,
            String aInCtrSNRState,
            CounterInstanceClass aInCounterInstanceClass,
            EntityCounterInstanceId aInEntityCounterInstanceId,
            boolean aInFirstUseCounter, String aInSubscriptionInstId)
    {
        TPAResources.debug(logger, "fillSyResultAnswerInfo started for ",
                "counter: ", aInCounterInstanceClass.getCounterDefId());

        if (aInCounterInstanceClass.getDeletionTime() != null || isSubscriptionExpired(aInSubscriptionInstId, aInCounterInstanceClass))
        {
            TPAResources.info(logger, " Unable to send SNR for the " ,
                    "counter instance of ",
                    aInCounterInstanceClass.getCounterDefId(),
                    " with id ", aInCounterInstanceClass.getCounterInsId(),
                    " as it is expired");
            return;
        }

        RatingEngineAnswerImpl lRatingEngineAnswer = (RatingEngineAnswerImpl) aInRatingEngineRequest
                .getRatingEngineAnswer();
        List<SyResultAnswer> lSyResultAnswers = lRatingEngineAnswer
                .getSyResultAnswerList();
        String lAccountId = null;
        if (lRatingEngineAnswer.getAccountId() != null)
        {
            lAccountId = lRatingEngineAnswer.getAccountId();
        }

        String lOldSignalingState = null;
        EntityCounterInstance lEntityCounterInstance = null;
        if (ThreadLocalCommonContext.getIsGlobalGroupSiteCall() &&
                aInRatingEngineRequest.getGlobalGroupCounterMap() != null &&
                aInRatingEngineRequest.getGlobalGroupCounterMap()
                        .containsKey(aInEntityCounterInstanceId))
        {
            lEntityCounterInstance = aInRatingEngineRequest.getGlobalGroupCounterMap()
                    .get(aInEntityCounterInstanceId);
        }
        else
        {
            lEntityCounterInstance =
                    Services.lookup(EntityCounterInstanceService.class).read(
                            aInEntityCounterInstanceId);
        }
        lOldSignalingState = updatePolicyCounterInstanceList(aInCounterInstanceClass,
                lEntityCounterInstance, aInSubscriptionInstId,
                aInRatingEngineRequest.getCall().getDeviceContext().getDevice(),
                aInCtrSNRState);
        CommonCallContext lCommonCallContext= aInRatingEngineRequest.getCall().getCommonCallContext();

        fillSyResultAnswerInfo(lSyResultAnswers, aInCtrSNRState,
                aInCounterInstanceClass,
                aInEntityCounterInstanceId, aInFirstUseCounter,
                aInSubscriptionInstId, lAccountId, false, lCommonCallContext, lOldSignalingState,
                lEntityCounterInstance);
    }

    /**
     * Method to fill SyResultAnswerInfo
     *
     * @param aInSyResultAnswers SyResultAnswers
     * @param aInCtrSNRState SNR state for the passed counter
     * @param aInCounterInstanceClass CounterInstanceClass
     * @param aInEntityCounterInstanceId EntityCounterInstanceId
     * @param aInFirstUseCounter true if FirstUse else false
     * @param aInSubscriptionInstId subscription id
     * @param aInAccountId account Id
     * @param isLifeCycle call from lifecycle or not
     * @param aInCommonCallContext CommonCallContext
     * @param aInOldSignalingState Old Signaling state value
     * @param aInEntityCounterInstance ECI Instance Object
     */
    public static void fillSyResultAnswerInfo(
            List<SyResultAnswer> aInSyResultAnswers,
            String aInCtrSNRState,
            CounterInstanceClass aInCounterInstanceClass,
            EntityCounterInstanceId aInEntityCounterInstanceId,
            boolean aInFirstUseCounter,
            String aInSubscriptionInstId, String aInAccountId,
            boolean isLifeCycle, CommonCallContext aInCommonCallContext,
            String aInOldSignalingState, EntityCounterInstance aInEntityCounterInstance)
    {
        TPAResources.debug(logger, "fillSyResultAnswerInfo started for ",
                "counter: ", aInCounterInstanceClass.getCounterDefId());

        List<SyResultAnswer> lSyResultAnswers = aInSyResultAnswers;
        SyResultAnswer lSyResultAnswer = new SyResultAnswer();
        lSyResultAnswer
                .setCounterInstId(aInCounterInstanceClass.getCounterInsId());
        lSyResultAnswer
                .setCounterDefId(aInCounterInstanceClass.getCounterDefId());
        lSyResultAnswer.setCounterInstanceClass(aInCounterInstanceClass);
        lSyResultAnswer.setSNRType(SNRType.CALL);

        String lCtrSignalState = null;
        String lDVPCName = null;
        if (VirtualPolicyCounter.BUCKET_COUNTER.equals(
                GlobalConfig.getVirtualPolicyCounter()))
        {
            Map<String, Pair<String, String>> lCountersForSNR =
                    PolicyCounterUtility.parseInputCounters(aInCtrSNRState);
            if (!lCountersForSNR.isEmpty())
            {
                Map.Entry<String, Pair<String, String>> lCounterEntry =
                        lCountersForSNR.entrySet().iterator().next();
                lDVPCName = lCounterEntry.getKey();
                lCtrSignalState = lCounterEntry.getValue().first;
            }
            fillUsageInSyResultAnswer(aInCommonCallContext,lSyResultAnswer);
        }


        Subscription lSubscription = null;
        // In case subscription-id is null then look for aInCounterInstanceClass
        if (aInSubscriptionInstId == null)
        {
            aInSubscriptionInstId = aInCounterInstanceClass.getSubsId();
        }
        if(aInSubscriptionInstId != null)
        {
            lSubscription = getSubscription(aInSubscriptionInstId);
        }

        if (aInAccountId == null)
        {
            if (lSubscription != null && lSubscription.getAccount() != null)
            {
                lSyResultAnswer.setAccountId(lSubscription.getAccount().getId());
            }
            else
            {
                EntityCounterInstance lEntityCounterInstance = null;
                if (aInEntityCounterInstance != null)
                {
                    lEntityCounterInstance = aInEntityCounterInstance;
                }
                else
                {
                    lEntityCounterInstance = Services.lookup(EntityCounterInstanceService.class)
                            .read(aInEntityCounterInstanceId);
                }

                if (Objects.nonNull(lEntityCounterInstance) && Objects.nonNull
                        (lEntityCounterInstance.getAccount()))
                {
                    Account lAccount = Services.lookup(AccountService.class)
                            .read(lEntityCounterInstance.getAccount().getId());
                    if (lAccount != null)
                    {
                        lSyResultAnswer.setAccountId(lAccount.getId());
                    }
                }
            }
        }
        else
        {
            lSyResultAnswer.setAccountId(aInAccountId);
        }

        if (GlobalConfig.isAlwaysSendLatestPCStatusInSNR())
        {
            if (aInOldSignalingState != null)
            {
                lSyResultAnswer.setOldSignalingState(aInOldSignalingState);
            }
            else if (lSubscription != null)
            {
                addSignalingStateInSyResultAnsFrmPolicyCounterInfo(false,
                        aInCounterInstanceClass.getCounterInsId(), aInEntityCounterInstanceId,
                        lSyResultAnswer, lSubscription, aInEntityCounterInstance, false);
            }
        }

        // Sorting Core Attributes will be filled if FLAG is ON
        if (VirtualPolicyCounter.BUCKET_COUNTER.equals(
                GlobalConfig.getVirtualPolicyCounter()))
        {
            if (lDVPCName != null && PolicyCounterUtility.isDynamicPolicyCounterEnabled())
            {
                lSyResultAnswer.setPolicyCounterName(lDVPCName);
            }
            else
            {
                lSyResultAnswer.setPolicyCounterName(
                        aInCounterInstanceClass.getPolicyCounterName());
            }
            if(lSubscription != null)
            {
                lSyResultAnswer.setIsBarred(lSubscription.isBarred());
                lSyResultAnswer.setSubsCreationOrExpiryTime(
                        fillCreationOrExpiryTime(lSubscription));
            }

            //  In case of Device Counter, Group Counter and Device_Group Counter
            //  set priority from GlobalConfig
            EntityCounterInstanceAttachMode lECIAttachMode =
                    aInEntityCounterInstanceId.getEntityCounterInstanceAttachMode();
            if(lECIAttachMode == EntityCounterInstanceAttachMode.DEVICE ||
                  lECIAttachMode == EntityCounterInstanceAttachMode.GROUP ||
                  lECIAttachMode == EntityCounterInstanceAttachMode.DEVICE_GROUP)
            {
                lSyResultAnswer.setPriority(
                                BigDecimal.valueOf(GlobalConfig.getAggregateCounterPriority()));
            }
            else
            {
                lSyResultAnswer.setPriority(getChargingServicePriority(
                        aInCounterInstanceClass.getChargingServiceId()));
            }
            lSyResultAnswer.setExhaustionTime(
                    aInCounterInstanceClass.getExhaustionTime());
        }
        TPAResources.debug(logger, "SNRType : "
                              ,lSyResultAnswer.getSNRType());

        if (aInCtrSNRState == null)
        {
            Counter lCounter = Services.lookup(CounterService.class)
                    .read(aInCounterInstanceClass.getCounterDefId());
            if (lCounter!= null && lCounter.isResourceBased())
            {
                lSyResultAnswer.setSignalingState(EntityCounterUtility
                        .fetchResourceCounterState(aInCounterInstanceClass));
                addSignalingStateInSyResultAnsFrmPolicyCounterInfo(false,
                        aInCounterInstanceClass.getCounterInsId(), aInEntityCounterInstanceId,
                        lSyResultAnswer, lSubscription, aInEntityCounterInstance, true);
                lSyResultAnswer.setResetSignalingState(lSyResultAnswer.getSignalingState());
            }
            else
            {
                if (isLifeCycle)
                {
                    lSyResultAnswer.setSignalingState(aInCounterInstanceClass.getLastSNRReportingState());
                }
                else
                {
                    SignalingState lSignalingState = entityCounterInstanceService
                            .findSignalingStateOfCounterInstance(
                                    aInCounterInstanceClass);
                    String lCurrentSignalingState = null;
                    String lResetSignalingState = null;
                    String lSNRReportingState = aInCounterInstanceClass.getLastSNRReportingState();
                    if (lSignalingState == null && lSNRReportingState != null)
                    {
                        TPAResources.debug(logger,
                                "Updating Signaling state of ",
                                aInCounterInstanceClass.getCounterDefId(),
                                " with entity counter insatance state [",
                                lSNRReportingState, "]");
                        lCurrentSignalingState = lSNRReportingState;
                        lResetSignalingState = lSNRReportingState;
                    }
                    else if (null != lSignalingState)
                    {
                        lCurrentSignalingState =
                                lSignalingState.getCurrentSignalingState();
                        lResetSignalingState =
                                lSignalingState.getResetSignalingState();
                    }
                    lSyResultAnswer
                            .setSignalingState(lCurrentSignalingState);
                    setOldSignalingStateWithResetValue(lSyResultAnswer, lResetSignalingState);

                    lSyResultAnswer.setResetSignalingState(lResetSignalingState);
                }
            }
            lSyResultAnswer.setFirstUseCounter(aInFirstUseCounter);
            TPAResources.debug(logger," SyResultAnswer = ", lSyResultAnswer);
            TPAResources.debug(logger, "First Use added For ", "counter: ",
                    aInCounterInstanceClass.getCounterDefId());
        }
        else
        {
            if (aInCounterInstanceClass.getCurrentValue()
                    .equals(BigDecimal.ZERO))
            {
                lSyResultAnswer.setFirstUseCounter(true);
                TPAResources.debug(logger, "First Use added For 0 value ",
                        "counter: ", aInCounterInstanceClass.getCounterDefId());
            }
            //state will be set for counters vpc & dvpc
            if (VirtualPolicyCounter.BUCKET_COUNTER.equals(
                    GlobalConfig.getVirtualPolicyCounter()))
            {
                if (lCtrSignalState == null)
                {
                    Map<String, String> lCountersForSNR =
                            PolicyCounterUtility.parseInputCounters(aInCtrSNRState,
                                    new SNRContext(null,null,
                                            aInCounterInstanceClass.getCounterDefId(),
                                            aInCounterInstanceClass.getCounterInsId()));
                    lCtrSignalState = lCountersForSNR.values().iterator().next();
                }
                lSyResultAnswer.setSignalingState(lCtrSignalState);
            }
            else
            {
                lSyResultAnswer.setSignalingState(aInCtrSNRState);
            }
            SignalingState lSignalingState = Services.lookup(EntityCounterInstanceService.class).
                    findSignalingStateOfCounterInstance(aInCounterInstanceClass);
            String lResetState = null;
            if (null != lSignalingState)
            {
                lResetState = lSignalingState.getResetSignalingState();
            }
            //If old Signaling state is null, reset with ResetSignalingState
            setOldSignalingStateWithResetValue(lSyResultAnswer, lResetState);
            lSyResultAnswer.setResetSignalingState(lResetState);
            TPAResources.debug(logger, "Non-First Use, Threshold case For ",
                    "counter: ", aInCounterInstanceClass.getCounterDefId(),
                    "State : ", lSyResultAnswer.getSignalingState(),
                    " reset signalingstate object = ", lResetState);
        }
        lSyResultAnswer.setEntityCounterInstanceId(aInEntityCounterInstanceId);
        lSyResultAnswer.setSendSNRRequired(true);

        if (aInCounterInstanceClass.getEndTime() == null)
        {
            try
            {
                if (aInEntityCounterInstanceId
                        .getEntityCounterInstanceAttachMode() ==
                        EntityCounterInstanceAttachMode.SUBSCRIPTION)
                {
                    lSyResultAnswer.getCounterInstanceClass()
                            .setEndTime(entityCounterInstanceService
                                    .getEndTimeOfSubscription(
                                            aInEntityCounterInstanceId));
                }
                else if (aInEntityCounterInstanceId
                        .getEntityCounterInstanceAttachMode() ==
                        EntityCounterInstanceAttachMode.MULTI_BUNDLE)
                {
                    lSyResultAnswer.getCounterInstanceClass()
                            .setEndTime(entityCounterInstanceService
                                    .getEndTimeOfSubscription(
                                            aInEntityCounterInstanceId,
                                            aInCounterInstanceClass
                                                    .getSubsId()));
                }
                else
                {
                    lSyResultAnswer.getCounterInstanceClass()
                            .setEndTime(entityCounterInstanceService
                                    .getEndTimeForGlobalCounters(
                                            aInEntityCounterInstanceId));
                }
            }
            catch (OperationFailedException aInException)
            {
                TPAResources.debug(logger, "End Time can not be populated.");
            }
        }

        if (aInCounterInstanceClass.getStartTime() == null)
        {
            setCounterStartTime(aInCounterInstanceClass, aInEntityCounterInstanceId, lSyResultAnswer);
        }


        TPAResources.debug(logger, "CounterInfo Values set in SyRsultAnswer ",
                " CounterInstanceId:- ", lSyResultAnswer.getCounterInstId(),
                " CounterDefinitionID:- ", lSyResultAnswer.getCounterDefId(),
                " CounterSignalingState:- ",
                lSyResultAnswer.getSignalingState(), " CounterAttached to :- ",
                lSyResultAnswer.getEntityCounterInstanceId());

        if (lDVPCName != null && PolicyCounterUtility.isDynamicPolicyCounterEnabled())
        {
            SignalingState lSignalingState = null;
            if (aInCtrSNRState != null)
            {
                lSignalingState = new SignalingState();
                lSignalingState.setCurrentSignalingState(aInCtrSNRState);
            }
            addDPCInfoInSyResultAnswer(lSyResultAnswer, lSyResultAnswers,
                    lSignalingState);
            TPAResources.debug(logger, "fillSyResultAnswerInfo ended for ",
                    "dynamic counter: ", lSyResultAnswer.getPolicyCounterName());
        }
        else
        {
            addCounterInfoInSyResultAnswer(lSyResultAnswer, lSyResultAnswers,
                    aInCtrSNRState);

            TPAResources.debug(logger, "fillSyResultAnswerInfo ended for ",
                    "counter: ", aInCounterInstanceClass.getCounterDefId());
        }
    }

    private static void setCounterStartTime(
            CounterInstanceClass aInCounterInstanceClass,
            EntityCounterInstanceId aInEntityCounterInstanceId,
            SyResultAnswer aInSyResultAnswer)
    {
        try
        {
            if (aInEntityCounterInstanceId
                    .getEntityCounterInstanceAttachMode() ==
                    EntityCounterInstanceAttachMode.SUBSCRIPTION)
            {
                aInSyResultAnswer.getCounterInstanceClass()
                        .setStartTime(
                                entityCounterInstanceService
                                        .getStartTimeOfSubscription(aInEntityCounterInstanceId)
                        );
            }
            else if (aInEntityCounterInstanceId
                    .getEntityCounterInstanceAttachMode() ==
                    EntityCounterInstanceAttachMode.MULTI_BUNDLE)
            {
                aInSyResultAnswer.getCounterInstanceClass()
                        .setStartTime(entityCounterInstanceService
                                .getStartTimeOfSubscription(
                                        aInEntityCounterInstanceId,
                                        aInCounterInstanceClass
                                                .getSubsId()));
            }
            else
            {
                aInSyResultAnswer.getCounterInstanceClass()
                        .setStartTime(entityCounterInstanceService
                                .getStartTimeForGlobalCounters(
                                        aInEntityCounterInstanceId));
            }
        }
        catch (OperationFailedException aInException)
        {
            TPAResources.errorException(logger, aInException, "Start Time can not be populated.");
        }
    }

    /**
     * Method to set oldSignaling state in SyResultAnswer with resetSignaling state value,
     * if it is already not set.
     * @param aInSyResultAnswer         SyResultAnswer
     * @param aInResetSignalingState    Reset Signaling State
     */
    private static void setOldSignalingStateWithResetValue(SyResultAnswer aInSyResultAnswer,
            String aInResetSignalingState)
    {
        //If old Signaling state is null, reset with ResetSignalingState
        if (aInSyResultAnswer.getOldSignalingState() == null && aInResetSignalingState != null)
        {
            aInSyResultAnswer.setOldSignalingState(aInResetSignalingState);
        }
    }

    /**
     * This method is used to add oldState in SyResultAnswer. This old state is used for evaluating
     * Policy Counter status change rule set.
     * @param aInForBucket              Flag to indicate if
     * @param aInInstanceId             Instance Id
     * @param aInEntityCounterInstanceId  Entity Counter Instance
     * @param aInOutSyResultAnswer      SyResultAnswer
     * @param aInSubscription           Subscription
     * @param aInEntityCounterInstance ECI object
     */
    private static void addSignalingStateInSyResultAnsFrmPolicyCounterInfo(boolean aInForBucket,
                                                                           String aInInstanceId, EntityCounterInstanceId aInEntityCounterInstanceId,
                                                                           SyResultAnswer aInOutSyResultAnswer, Subscription aInSubscription,
                                                                           EntityCounterInstance aInEntityCounterInstance, boolean aInIsSetNewState)
    {
        TPAResources.debug(logger, "Add OldState in SyResultAns: ",
                aInOutSyResultAnswer,", for InstanceId: ", aInInstanceId, ", isBucket: ",
                aInForBucket, ", ECI: ", aInEntityCounterInstanceId,
                ", Subscription: ", aInSubscription != null ? aInSubscription.getId() : null);
        try
        {
            EntityCounterInstance lEntityCounterInstance = null;
            if (!aInForBucket)
            {
                if (aInEntityCounterInstance != null)
                {
                    lEntityCounterInstance = aInEntityCounterInstance;
                }
                else
                {
                    lEntityCounterInstance =
                            Services.lookup(EntityCounterInstanceService.class).read(
                                    aInEntityCounterInstanceId);
                }
            }
            String lPCIKey = PolicyCounterInstanceUtility.getPCIKey(
                    lEntityCounterInstance, aInSubscription);

            PolicyCounterInstance lPolicyCounterInstanceFromMap =
                    (PolicyCounterInstance) ThreadLocalCommonContext
                            .getPolicyCtrInsMap().get(lPCIKey);
            PolicyCounterInstance lPolicyCounterInstanceDB;

            if (lPolicyCounterInstanceFromMap == null)
            {
                PolicyCounterInstanceService lPolicyCounterInstanceService =
                        Services.lookup(PolicyCounterInstanceService.class);

                lPolicyCounterInstanceDB = lPolicyCounterInstanceService
                        .readPolicyCounterInstance(lEntityCounterInstance,
                                aInSubscription);
                if (lPolicyCounterInstanceDB != null)
                {
                    TPAResources.debug(logger, "Store Policy Counter instance: ",
                            lPolicyCounterInstanceDB, " for Key: ", lPCIKey,
                            " into SyResultAnswer.");
                    Map<String, PolicyCounterInstance> lPCIMap = new HashMap<>();
                    lPCIMap.put(lPCIKey, lPolicyCounterInstanceDB);
                    //This is used to update oldState in DB, when policy counter status change
                    // ruleset is triggered and thread local is empty.
                    aInOutSyResultAnswer.setPolicyCounterInstanceMap(lPCIMap);
                }
            }
            else
            {
                TPAResources.debug(logger, "Policy Counter instance: ",
                        lPolicyCounterInstanceFromMap, " found from 'policyCtrInsMap' for Key: ",
                        lPCIKey);
                lPolicyCounterInstanceDB = lPolicyCounterInstanceFromMap;
            }

            if (lPolicyCounterInstanceDB != null)
            {
                TPAResources.debug(logger, "PolicyCounterInstanceDB is :",
                        lPolicyCounterInstanceDB);
                for (PolicyCounterInfo lPolicyCounterInfo : lPolicyCounterInstanceDB
                        .getCurrentPolicyCounterInfos())
                {
                    if ((aInForBucket && lPolicyCounterInfo.getBktInsId() != null
                        && lPolicyCounterInfo.getBktInsId().equals(aInInstanceId)) ||
                            (!aInForBucket && aInInstanceId.equals(lPolicyCounterInfo.getCtrInsId())))
                    {
                            if (!aInIsSetNewState)
                            {
                                aInOutSyResultAnswer.setOldSignalingState(
                                        lPolicyCounterInfo.getOldSNRReportingState());
                                TPAResources.debug(logger, "Old signaling ",
                                        "state is set in SyResultAnswer : ", aInOutSyResultAnswer);
                            }
                            else
                            {
                                aInOutSyResultAnswer.setSignalingState(
                                        lPolicyCounterInfo.getLastSNRReportingState());
                                TPAResources.debug(logger, "New signaling ",
                                        "state is set in SyResultAnswer : ", aInOutSyResultAnswer);
                            }
                        aInOutSyResultAnswer.setPrevBundleName(
                                lPolicyCounterInfo.getPrevBundleName());
                    }
                }
            }
        }
        catch (OperationFailedException aInE)
        {
            TPAResources.errorException(logger, aInE, "Exception occurred while adding",
                    " Signaling state in SyResultAnswer");
        }
    }

    /**
     * Method to fill SyResultAnswerInfo for bucketInstance
     *
     * @param aInRatingEngineRequest RatingEngineRequest
     * @param aInCtrSNRState SNR state for the passed counter
     * @param aInBucketInstance BucketInstance of policy type.
     * @param aInFirstUseCounter true if FirstUse else false
     * @param aInSubscription subscription object
     */
    public static void fillSyResultAnswerInfoForBucketInst(
            RatingEngineRequest aInRatingEngineRequest, SignalingState aInCtrSNRState,
            BucketInstance aInBucketInstance, boolean aInFirstUseCounter,
            Subscription aInSubscription)
    {
        TPAResources.debug(logger, "fillSyResultAnswerInfo started for ",
                "bucket: ", aInBucketInstance.getBucketDefId());

        RatingEngineAnswerImpl lRatingEngineAnswer = (RatingEngineAnswerImpl)
                aInRatingEngineRequest.getRatingEngineAnswer();
        List<SyResultAnswer> lSyResultAnswers = lRatingEngineAnswer
                .getSyResultAnswerList();

        if (aInFirstUseCounter &&
                checkPolicyBucketExistInContext(aInBucketInstance,
                lSyResultAnswers))
        {
            return;
        }
        // Set SyResultAnswer
        SyResultAnswer lSyResultAnswer = new SyResultAnswer();
        lSyResultAnswer.setBucketDefId(aInBucketInstance.getBucketDefId());
        lSyResultAnswer.setBucketInstId(aInBucketInstance.getId());
        lSyResultAnswer.setBucketInstance(aInBucketInstance);
        lSyResultAnswer.setSNRType(SNRType.CALL);
        lSyResultAnswer.setSendSNRRequired(true);
        if (aInSubscription != null && GlobalConfig.isAlwaysSendLatestPCStatusInSNR())
        {
            addSignalingStateInSyResultAnsFrmPolicyCounterInfo(true,
                    ClubbingUtility.getBucketInstanceBusinessId(aInBucketInstance.getId()),
                    null, lSyResultAnswer, aInSubscription,null, false);
        }
        // Sorting Core Attributes will be filled if FLAG is ON
        if (VirtualPolicyCounter.BUCKET_COUNTER.equals(
                GlobalConfig.getVirtualPolicyCounter()))
        {

            CommonCallContext lCommonCallContext = aInRatingEngineRequest.getCall().getCommonCallContext();

            fillUsageInSyResultAnswer(lCommonCallContext, lSyResultAnswer);

            lSyResultAnswer.setPolicyCounterName(
                    aInBucketInstance.getPolicyCounterName());

            /** Priority-Specific Attributes*/
            if (aInSubscription != null)
            {
                lSyResultAnswer.setIsBarred(aInSubscription.isBarred());
                lSyResultAnswer.setSubsCreationOrExpiryTime(
                        fillCreationOrExpiryTime(aInSubscription));
            }
            lSyResultAnswer.setPriority(getChargingServicePriority(
                    aInBucketInstance.getChargingServiceId()));
            lSyResultAnswer.setExhaustionTime(aInBucketInstance.getExhaustionTime());
            lSyResultAnswer.setAccountId(aInRatingEngineRequest.
                    getRatingEngineAnswer().getAccountId());
            if(aInRatingEngineRequest.getRatingEngineAnswer().getAccountId() == null &&
                    aInSubscription != null && aInSubscription.getAccount() != null)
            {
                lSyResultAnswer.setAccountId(aInSubscription.getAccount().getId());
            }
            else
            {
                lSyResultAnswer.setAccountId(aInRatingEngineRequest.
                        getRatingEngineAnswer().getAccountId());
            }
        }
        TPAResources.debug(logger, "SNRType : " ,lSyResultAnswer.getSNRType());

        if (aInCtrSNRState == null)
        {
            // TODO: Handle first use for bucket.
        }
        else
        {
            // TODO: Handle first use for bucket.
            lSyResultAnswer.setSignalingState(aInCtrSNRState.getCurrentSignalingState());
            lSyResultAnswer.setResetSignalingState(aInCtrSNRState.getResetSignalingState());
            TPAResources.debug(logger, "Non-First Use, Threshold case For ",
                    "counter: ", aInBucketInstance.getBucketDefId(),
                    "State : ", lSyResultAnswer.getSignalingState(),
                    " and reset state = ", aInCtrSNRState.getResetSignalingState());
        }

        TPAResources.debug(logger, "BucketInfo Values set in SyResultAnswer ",
                " BucketInstanceId:- ", lSyResultAnswer.getBucketInstId(),
                " BucketDefinitionID:- ", lSyResultAnswer.getBucketDefId(),
                " PolicyCounterName:- ", lSyResultAnswer.getPolicyCounterName(),
                " CounterSignalingState:- ", lSyResultAnswer
                        .getSignalingState());

        if (lSyResultAnswer.getPolicyCounterName() != null &&
                PolicyCounterUtility.isDynamicPolicyCounterEnabled())
        {
            addDPCInfoInSyResultAnswer(lSyResultAnswer, lSyResultAnswers,
                    aInCtrSNRState);
            TPAResources.debug(logger, "fillSyResultAnswerInfo ended for ",
                    "dynamic counter: ", lSyResultAnswer.getPolicyCounterName());
        }
        else
        {
            addBucketInfoInSyResultAnswer(lSyResultAnswer, lSyResultAnswers,
                    aInCtrSNRState);
            TPAResources.debug(logger, "fillSyResultAnswerInfo ended for ",
                    "counter: ", aInBucketInstance.getBucketDefId());
        }
    }

    /**
     * Method to check if sy result answer already exist for patricular entitiy
     * @param aInBucketInstance  BucketInstance
     * @param aInSyResultAnswers SyResultAnswer
     * @return true if exist else false
     */
    private static boolean checkPolicyBucketExistInContext(BucketInstance aInBucketInstance ,
            List<SyResultAnswer> aInSyResultAnswers)
    {
        for (SyResultAnswer lSyResultAnswer : aInSyResultAnswers)
        {
            if (null != lSyResultAnswer.getBucketInstId() &&
                    aInBucketInstance.getId().equals(lSyResultAnswer.getBucketInstId()))
            {
                TPAResources.debug(logger, "BucketInstance's context sy result " ,
                        "answer is already added.");
                return true;
            }
        }
        return false;
    }

    /**
     * Method to check whether counter already exists in SyResultAnswer
     *
     * @param counterInsId Counter Instance ID to be checked
     * @param aInOutSyResultAnswers SyResultAnswer Objects
     * @return true means already exists in SyResultAnswer.
     */
    public static boolean isCounterExistsInSyResultAnswer(String counterInsId,
            List<SyResultAnswer> aInOutSyResultAnswers)
    {
        boolean lCounterExists = false;

        for (SyResultAnswer lSyResultAnswer : aInOutSyResultAnswers)
        {
            if (counterInsId.equals(lSyResultAnswer.getCounterInstId()))
            {
                TPAResources.debug(logger, "Counter Instance Id ", counterInsId,
                        " already exists  in SyResultAnswers");
                lCounterExists = true;
                break;
            }
        }
        return lCounterExists;
    }

    /**
     * Method to add or replace Counter Infos in SyResultAnswer
     *
     * @param aInSyResultAnswer SyResultAnswer
     * @param aInOutSyResultAnswers SyResultAnswer Objects
     * @param aInCtrSNRState SNR state for the passed counter
     */
    private static void addCounterInfoInSyResultAnswer(
            SyResultAnswer aInSyResultAnswer,
            List<SyResultAnswer> aInOutSyResultAnswers,
            String aInCtrSNRState)
    {
        TPAResources.debug(logger, " Adding Counter in SyResultAnswer: ",
                aInSyResultAnswer.getCounterDefId());

        boolean lCounterExists = false;

        if (aInCtrSNRState == null)
        {
            lCounterExists = isCounterExistsInSyResultAnswer(
                    aInSyResultAnswer.getCounterInstId(),
                    aInOutSyResultAnswers);

            if (!lCounterExists)
            {
                aInOutSyResultAnswers.add(aInSyResultAnswer);
            }
        }
        else
        {
            for (SyResultAnswer lSyResultAnswer : aInOutSyResultAnswers)
            {
                if (aInSyResultAnswer.getCounterInstId()
                        .equals(lSyResultAnswer.getCounterInstId()))
                {
                    lCounterExists = true;
                    aInOutSyResultAnswers.set(
                            aInOutSyResultAnswers.indexOf(lSyResultAnswer),
                            aInSyResultAnswer);
                    break;
                }
            }

            if (!lCounterExists)
            {
                aInOutSyResultAnswers.add(aInSyResultAnswer);
            }
        }

        TPAResources.debug(logger, " Added  Counter in SyResultAnswer ",
                aInSyResultAnswer.getCounterDefId(), " Successfully");
    }

    /**
     * Method to add or replace dynamic policy counter Information in SyResultAnswer.
     *
     * @param aInSyResultAnswer     SyResultAnswer
     * @param aInOutSyResultAnswers SyResultAnswer Objects
     * @param aInCtrSNRState        SNR state for the passed counter
     */
    private static void addDPCInfoInSyResultAnswer(
            SyResultAnswer aInSyResultAnswer,
            List<SyResultAnswer> aInOutSyResultAnswers,
            SignalingState aInCtrSNRState)
    {
        if (aInSyResultAnswer.getPolicyCounterName() == null)
        {
            TPAResources.debug(logger, "SyResultAnswer called with Null " ,
                    "Policy Counter Name");
            return;
        }

        TPAResources.debug(logger, " Adding Dynamic Policy Counter in " ,
                "SyResultAnswer: ", aInSyResultAnswer.getPolicyCounterName());

        boolean lDPCExists = false;

        for (SyResultAnswer lSyResultAnswer : aInOutSyResultAnswers)
        {
            if (aInSyResultAnswer.getPolicyCounterName()
                    .equals(lSyResultAnswer.getPolicyCounterName()))
            {
                TPAResources.debug(logger, "Dynamic Policy Counter Name: ",
                        aInSyResultAnswer.getPolicyCounterName(), " already exists ",
                        "in SyResultAnswers");
                lDPCExists = true;

                if (aInCtrSNRState.getCurrentSignalingState() != null)
                {
                    aInOutSyResultAnswers.set(
                            aInOutSyResultAnswers.indexOf(lSyResultAnswer),
                            aInSyResultAnswer);
                }
                break;
            }
        }

        if (!lDPCExists)
        {
            aInOutSyResultAnswers.add(aInSyResultAnswer);
            TPAResources.debug(logger, " Added  Dynamic Policy Counter in " ,
                    "SyResultAnswer ", aInSyResultAnswer.getPolicyCounterName(), " Successfully");
        }
    }

    /**
     * Method to add or replace policy bucket Information in SyResultAnswer.
     *
     * @param aInSyResultAnswer SyResultAnswer
     * @param aInOutSyResultAnswers SyResultAnswer Objects
     * @param aInCtrSNRState SNR state for the passed counter
     */
    private static void addBucketInfoInSyResultAnswer(
            SyResultAnswer aInSyResultAnswer,
            List<SyResultAnswer> aInOutSyResultAnswers,
            SignalingState aInCtrSNRState)
    {
        TPAResources.debug(logger, " Adding Bucket in SyResultAnswer: ",
                aInSyResultAnswer.getBucketDefId());

        boolean lBucketExists = false;

        for (SyResultAnswer lSyResultAnswer : aInOutSyResultAnswers)
        {
            if (aInSyResultAnswer.getBucketInstId()
                    .equals(lSyResultAnswer.getBucketInstId()))
            {
                TPAResources.debug(logger, "Bucket Instance Id: ",
                        aInSyResultAnswer.getBucketInstId()," already exists ",
                        "in SyResultAnswers");
                lBucketExists = true;

                if (aInCtrSNRState.getCurrentSignalingState() != null)
                {
                    aInOutSyResultAnswers.set(
                            aInOutSyResultAnswers.indexOf(lSyResultAnswer),
                            aInSyResultAnswer);
                }
                break;
            }
        }

        if (!lBucketExists)
        {
            aInOutSyResultAnswers.add(aInSyResultAnswer);
        }

        TPAResources.debug(logger, " Added  Bucket in SyResultAnswer ",
                aInSyResultAnswer.getBucketDefId(), " Successfully");
    }

    /**
     * This method will do post work for counter for which notification is to be
     * sent during CCR-U/T. After post work, it will trigger rule engine by
     * calling {@link RuleEngineHandler#triggerRuleEngineForPostProcessing(REWrapper,
     * PostProcessingContextImpl, Map, SubscriptionContext, BundleContext)}
     *
     * @param aInCounterInstance {@link CounterInstanceClass} of counter for
     *                           which rule is triggered.
     * @param aInREWrapper       {@link REWrapper} object.
     * @param aInSourceContexts  is the source context required to evaluate
     *                           result
     * @param aInSubscriptionContext  Subscription context
     * @param aInBundleContext   Bundle context
     */
    public static void triggerRuleEngineForPostProcessingCounter(
            CounterInstanceClass aInCounterInstance, REWrapper aInREWrapper,
            Map<String, SourceContext> aInSourceContexts,
            SubscriptionContext aInSubscriptionContext, BundleContext aInBundleContext)
    {
        if (aInCounterInstance == null)
        {
            TPAResources.debug(logger, "Counter instance is null. Not "
                    , "processing rule for counter notification for CCR-U/T.");
            return;
        }
        PostProcessingContextImpl lPostProcessingContext = aInREWrapper
                .getContextManagement().getCounterPostProcessingContexts()
                .get(aInCounterInstance.getCounterInsId());
        TPAResources.debug(logger, "Post processing " ,
                "context for counter instance: " , aInCounterInstance
                .getCounterInsId() , " is : " , lPostProcessingContext);
        triggerRuleEngineForPostProcessing(aInREWrapper, lPostProcessingContext,
                aInSourceContexts, aInSubscriptionContext, aInBundleContext, null);
    }

    /**
     * This method is to trigger rule engine logic for RatingPostProcess and ResourcePostProcess
     * during CCR-U/T.
     *
     * @param aInREWrapper {@link REWrapper} object.
     * @param aInPostProcessingContext {@link PostProcessingContextImpl}
     *            of bucket/counter for which rule is triggered.
     * @param aInSourceContexts is the source context required to evaluate result
     * @param aInSubscriptionContext  Subscription context
     * @param aInBundleContext  Bundle context
     */
    public static void triggerRuleEngineForPostProcessing(
            REWrapper aInREWrapper, PostProcessingContextImpl aInPostProcessingContext,
            Map<String, SourceContext> aInSourceContexts,
            SubscriptionContext aInSubscriptionContext,
            BundleContext aInBundleContext, Boolean aInResourceOnly)
    {
        RatingEngineRequest lReRequest = aInREWrapper.getRatingEngineRequest();
        checkWholesaleCLOnTerminateIMS(lReRequest);
        // Source context
        TPAResources.debug(logger, "Populating Source Context for Rule ");
        aInSourceContexts.put(SourceContextType.SPR_DEVICE,
                lReRequest.getCall().getDeviceContext());
        aInSourceContexts.put ( SourceContextType.SPR_USER ,
                    new UserContextImpl ( ) );
        aInSourceContexts.put(SourceContextType.CALL_COMMON,
                lReRequest.getCall().getCommonCallContext());
        aInSourceContexts.put(SourceContextType.GY_MESSAGE,
                lReRequest.getCall().getGyMessageContext());
        aInSourceContexts.put(SourceContextType.NCHF_CHARGING_MESSAGE,
                lReRequest.getCall().getNchfChargingMessageContext());
        String lAccountId = findAccountIdForPostTrigger(
                lReRequest,
                aInPostProcessingContext,
                aInSubscriptionContext);
        aInSourceContexts.put(SourceContextType.SPR_ACCOUNT,
                createAccountContextFromAccountId(lAccountId));

        if(!CommonUtil.isNullOrEmpty(lReRequest.getCall().getEcommerceCallContext()))
        {
            aInSourceContexts.put(SourceContextType.ECOMMERCE,
                    lReRequest.getCall().getEcommerceCallContext());
        }

        if (aInPostProcessingContext.getResourceTriggerType().equals(ResourceTriggerType.COMMIT)
                && GlobalConfig.isNotificationCustomFlag()
                && !Boolean.TRUE.equals(aInResourceOnly))
        {

            triggerRuleEngineFoRatingPostProcess(aInREWrapper,
                    aInPostProcessingContext, aInSourceContexts);
        }
        if (aInSubscriptionContext != null)
        {
            aInSourceContexts.put(SourceContextType.SUBSCRIPTION,
                    aInSubscriptionContext);
            aInSourceContexts.put(SourceContextType.BUNDLE,
                    aInBundleContext);
        }
        triggerRuleEngineFoResourcePostProcess(aInREWrapper,
                aInPostProcessingContext, aInSourceContexts);
    }

    /**
     * This method is used to evaluate RatingPostProcess trigger
     *
     * @param aInREWrapper {@link REWrapper} object.
     * @param aInRatingPostProcessingContext {@link RatingPostProcessingContext}
     *            of bucket/counter for which rule is triggered.
     * @param aInSourceContexts is the source context required to evaluate result
     */
    private static void triggerRuleEngineFoRatingPostProcess(REWrapper aInREWrapper,
            RatingPostProcessingContext aInRatingPostProcessingContext,
            Map<String, SourceContext> aInSourceContexts)
    {
        NotificationCollectionImpl lNotificationCollections =
                (NotificationCollectionImpl) aInREWrapper
                        .getRatingEngineRequest().getNotificationCollection();

        ChargingPolicyDecisionResult lResult =
                new ChargingPolicyDecisionResultImpl();
        RatingEngineRequest lReRequest = aInREWrapper.getRatingEngineRequest();
        RatingEngineAnswer lReAnswer = lReRequest.getRatingEngineAnswer();

        // Evaluate rule
        aInSourceContexts.put(SourceContextType.RATING_POSTPROCESSING,
                aInRatingPostProcessingContext);
        ChargingPolicyDecisionWorker.evaluate(
                RuleSetType.RATING_POST_PROCESSING, aInSourceContexts, lResult, null);
        if ( aInREWrapper.getRatingEngineRequest() != null && aInREWrapper.getRatingEngineRequest().getCall() != null
                && CommonUtil.isNotNullOrEmpty(aInREWrapper.getRatingEngineRequest().getCall().getEcommerceCallContext())
                && !com.google.common.base.Strings
                .isNullOrEmpty(aInREWrapper.getRatingEngineRequest().getCall().getEcommerceCallContext().getTransactionId()))
        {
            GenericValueWrapper lReportUsageWrapper = lResult
                    .getAttributeResult(ResultContextType.CALL_RESULT,
                            ChargingRuleAttributeName.REPORT_CHARGED_AMOUNT);
            TPAResources.debug(logger,"invoke ReportChargedAmount method from ",
                    "EcommerceCallContext for triggerRuleEngineFoRatingPostProcess method");
            aInREWrapper.getRatingEngineRequest().getCall().getEcommerceCallContext().
                    ReportChargedAmount(lReportUsageWrapper);
            TPAResources.debug(logger,"finish execute ReportChargedAmount method from ",
                    "EcommerceCallContext for triggerRuleEngineFoRatingPostProcess method");
        }
        if ( CommonUtil.isNotNullOrEmpty ( lResult.getResult () ) )
            handleAddCdrFromResultContext(lResult, lReRequest);
        RuleEngineHandler.addPolicyDecisionResultToAnswer(
                RuleSetType.RATING_POST_PROCESSING, lResult,lReAnswer);
        DeviceContext lDeviceContext = (DeviceContext) aInSourceContexts
                .get(SourceContextType.SPR_DEVICE);
        GroupContext lGroupContext = (GroupContext) aInSourceContexts
                .get(SourceContextType.SPR_GROUP);
        AccountContext lAccountContext = (AccountContext) aInSourceContexts
                .get(SourceContextType.SPR_ACCOUNT);
        CommonCallContext lCommonCallContext = (CommonCallContext) aInSourceContexts
                .get(SourceContextType.CALL_COMMON);
          aInSourceContexts.put(SourceContextType.SPR_USER,
                  new UserContextImpl ( ));

        // Execute Custom Data action
        CustomDataActionUtils.checkAndExecuteCustomDataAction(
                lDeviceContext, lGroupContext, lResult, lCommonCallContext);

        SubscriptionContext lSubscriptionContext = (SubscriptionContext)
                aInSourceContexts.get(SourceContextType.SUBSCRIPTION);
        if ((lSubscriptionContext != null) && (lSubscriptionContext.getSubscriptionId() != null))
        {
            Subscription lSubscription = Services.lookup(SubscriptionService.class)
                .read(lSubscriptionContext.getSubscriptionId());
            CustomDataActionUtils.checkAndExecuteCustomDataActionInSubscription(lSubscription, lResult);
        }

        if (aInRatingPostProcessingContext.getResourceType().equals(EntityTypeEnum.NOCHARGE.name())
                && isCounterApplicableForNoCharge(aInREWrapper.getContextManagement(),
                (PostProcessingContextImpl) aInRatingPostProcessingContext))
        {
            TPAResources.debug(logger,
                    "In case of charging done from NOCHARGE with any associated counter" ,
                            " counting the usage, notification would not be sent for resource NOCHARGE");

        }
        else
        {
            // Notification action should be exceuted for Resource NOCHARGE only if there is no associated counter
            executeNotificationAction(lResult, aInREWrapper,
                    aInRatingPostProcessingContext.getDiscountId(), lNotificationCollections,
                    lDeviceContext.getId(), false);
        }
        //rule logic set reporting reason for metrics in case CCRu/t
        handleReportingReasonMetricsResult(aInREWrapper, lResult);
    }

    /**
     * This method is used to evaluate ResourcePostProcess trigger
     *
     * @param aInREWrapper {@link REWrapper} object.
     * @param aInResourcePostProcessingContext {@link ResourcePostProcessingContext}
     *            of bucket/counter for which rule is triggered.
     * @param aInSourceContexts is the source context required to evaluate result
     */
    private static void triggerRuleEngineFoResourcePostProcess(REWrapper aInREWrapper,
            ResourcePostProcessingContext aInResourcePostProcessingContext,
            Map<String, SourceContext> aInSourceContexts)
    {

        NotificationCollectionImpl lNotificationCollections =
                (NotificationCollectionImpl) aInREWrapper
                        .getRatingEngineRequest().getNotificationCollection();

        ChargingPolicyDecisionResult lResult =
                new ChargingPolicyDecisionResultImpl();

        RatingEngineRequest lReRequest = aInREWrapper.getRatingEngineRequest();
        RatingEngineAnswer lReAnswer = lReRequest.getRatingEngineAnswer();
        // Evaluate rule
        aInSourceContexts.put(SourceContextType.RESOURCE_POST_PROCESSING,
                aInResourcePostProcessingContext);
        aInSourceContexts.put(SourceContextType.SESSION_PARAMETERS,
                lReRequest.getCall().getSessionParameterContext());
        aInSourceContexts.put(SourceContextType.IMS_SERVICE_INFO,
                lReRequest.getCall().getImsServiceInformationContext());
        ChargingPolicyDecisionWorker.evaluate(
                RuleSetType.RESOURCE_POST_PROCESSING, aInSourceContexts,
                lResult, null);

        ResourceTriggerType lResourceTriggerType = aInResourcePostProcessingContext.getResourceTriggerType();

        RuleEngineHandler.addPolicyDecisionResultToAnswer(
                RuleSetType.RESOURCE_POST_PROCESSING, lResult, lReAnswer);
        DeviceContext lDeviceContext = (DeviceContext) aInSourceContexts
                .get(SourceContextType.SPR_DEVICE);
        GroupContext lGroupContext = (GroupContext) aInSourceContexts
                .get(SourceContextType.SPR_GROUP);
        CommonCallContext lCommonCallContext = (CommonCallContext) aInSourceContexts
                .get(SourceContextType.CALL_COMMON);

        GyMessageContext lGyMessageContext =
                (GyMessageContext) aInSourceContexts
                        .get(SourceContextType.GY_MESSAGE);
        if ( aInREWrapper.getRatingEngineRequest() != null && aInREWrapper.getRatingEngineRequest().getCall() != null
                && CommonUtil.isNotNullOrEmpty(aInREWrapper.getRatingEngineRequest().getCall().getEcommerceCallContext())
                && !com.google.common.base.Strings
                .isNullOrEmpty(aInREWrapper.getRatingEngineRequest().getCall().getEcommerceCallContext().getTransactionId()))
        {
            GenericValueWrapper lReportUsageWrapper = lResult
                    .getAttributeResult(ResultContextType.CALL_RESULT,
                            ChargingRuleAttributeName.REPORT_CHARGED_AMOUNT);
            TPAResources.debug(logger,"invoke ReportChargedAmount method from ",
                    "EcommerceCallContext for triggerRuleEngineFoResourcePostProcess method");
            aInREWrapper.getRatingEngineRequest().getCall()
                    .getEcommerceCallContext().
                    ReportChargedAmount(lReportUsageWrapper);
            TPAResources.debug(logger,
                    "invoke ReportChargedAmount method from ",
                    "EcommerceCallContext for triggerRuleEngineFoResourcePostProcess method");
        }
        if (lResult
                .getContextResults(ResultContextType.SESSION_PARAMETERS) !=
                null)
        {
            //Add-CustomField-In-Session Action
            handleAddCustomFieldFromResultContext(lResult, lReRequest);
        }
        if (lResult.getContextResults(
                ResultContextType.RESOURCE_POST_PROCESSING) != null)
        {
            //Add-RESOURCE_POST_PROCESSING Actions
            handleTriggerLifeCycleActionForResourcePostProcessingFromResultContext(
                    lResult, (null != lGyMessageContext ?
                            lGyMessageContext.getSubscriptionIdData() :
                            null), lDeviceContext,
                    lGroupContext,
                    (lDeviceContext.getOwnerId()),
                    aInResourcePostProcessingContext.getBillingAccId() !=
                            null ?
                            aInResourcePostProcessingContext.getBillingAccId() :
                            null);
        }


        handleTriggerWholesaleActionForResourcePostProcessingFromResultContext(aInREWrapper ,lResult, aInResourcePostProcessingContext);
        if (isApplicableResourceTriggerType(lResult,
                aInResourcePostProcessingContext,
                lResourceTriggerType))
        {
            // Execute Custom Data action
            CustomDataActionUtils.checkAndExecuteCustomDataAction(
                    lDeviceContext, lGroupContext, lResult, lCommonCallContext);

            SubscriptionContext lSubscriptionContext = (SubscriptionContext)
                aInSourceContexts.get(SourceContextType.SUBSCRIPTION);
            if ((lSubscriptionContext != null) && (lSubscriptionContext.getSubscriptionId() != null))
            {
                Subscription lSubscription = Services.lookup(SubscriptionService.class)
                    .read(lSubscriptionContext.getSubscriptionId());
                CustomDataActionUtils.checkAndExecuteCustomDataActionInSubscription(lSubscription, lResult);
            }

            //Execute Notification action
            if (aInResourcePostProcessingContext.getResourceType()
                    .equals(EntityTypeEnum.NOCHARGE.name())
                    && isCounterApplicableForNoCharge(aInREWrapper.getContextManagement(),
                    (PostProcessingContextImpl) aInResourcePostProcessingContext))
            {
                TPAResources.debug(logger,
                        "In case of charging done from NOCHARGE with any counter counting" ,
                                " the usage, notification would not be sent for resource NOCHARGE");

            }
            else
            {
                // Notification action should be exceuted for Resource NOCHARGE only if there is no associated counter
                executeNotificationAction(lResult, aInREWrapper,
                        aInResourcePostProcessingContext.getDiscountId(),
                        lNotificationCollections,
                        lDeviceContext.getId(), false);
            }
            if (lResult.getContextResults(ResultContextType.CALL_RESULT) !=
                    null)
            {
                handleAddCdrFromResultContext(lResult, lReRequest);
                SSIDetail lSsiDetail = new AVPUtil().getSSIDetail(
                        lResult);
                if (CommonUtil.isNotNullOrEmpty(lSsiDetail))
                {
                    TPAResources.debug(logger,
                            "Set SSIDetail AVP in Gy Message Context.");
                    lReRequest.getCall().getGyMessageContext().setsSIDetail(lSsiDetail);
                }
            }
            if (null != lResult.getContextResults(ResultContextType.RATING))
            {
                handleAddingMetricsAttribute(aInREWrapper,lResult);
            }
        }
        gatherCopyCdrTagRules(lResult, lReRequest.getCdrEngine());
    }

    /**
     * This Method is used to handle Trigger-Life-Cycle-Event Rule attribute in
     * RESOURCE_POST_PROCESSING.
     *
     * @param aInResult        Charging Rule execution result
     * @param aInDeviceContext Device Context
     * @param aInGroupContext  Group Context
     * @param aInOwnerId       owner Id
     * @param aInAccountId     account Id
     */
    private static void handleTriggerLifeCycleActionForResourcePostProcessingFromResultContext(
            ChargingPolicyDecisionResult aInResult, String subscriptionId,
            DeviceContext aInDeviceContext,
            GroupContext aInGroupContext, String aInOwnerId,
            String aInAccountId)
    {

        List<GenericValueWrapper> lLifeCycleTriggerEvents = aInResult
                .getAttributeResult(ResultContextType.RESOURCE_POST_PROCESSING,
                        ChargingRuleAttributeName.TRIGGER_LIFE_CYCLE_EVENT);

        if (!com.alcatel.tpapps.common.utils.CommonUtil
                .isNullOrEmpty(lLifeCycleTriggerEvents))
        {
            RuleEngineHandler.triggerRuleEngineForResourcePostProcessing(
                    lLifeCycleTriggerEvents, subscriptionId, aInDeviceContext,
                    aInGroupContext, aInOwnerId, aInAccountId);
        }

    }

    /**
     * Returns true if the rule evaluates to RESOURCE_TRIGGER_APPLICABLE, and attribute is either defined as BOTH or
     * defined same as call is coming from whether for RESERVATION or COMMIT, else returns false.
     * @param lResult Evaluation of Rule result
     * @param aInResourcePostProcessingContext Resource post processing context
     * @return returned value true or false
     */
    private static boolean isApplicableResourceTriggerType(ChargingPolicyDecisionResult lResult,
            ResourcePostProcessingContext aInResourcePostProcessingContext,
            ResourceTriggerType aInResourceTriggerType)
    {
        ResourceTriggerApplicable lApplicableTriggerType = ResourceTriggerApplicable.COMMIT;
        if (lResult.getContextResults(ResultContextType.CALL_RESULT) != null)
        {
            ResourceTriggerApplicable lApplicableResourceTrigger = lResult.getAttributeResult(ResultContextType.CALL_RESULT,
                    ChargingRuleAttributeName.RESOURCE_TRIGGER_APPLICABLE);
            TPAResources.debug(logger, "ResourceTriggerType configuration= ",
                    lApplicableResourceTrigger);
            if (lApplicableResourceTrigger != null)
            {
                switch (lApplicableResourceTrigger)
                {
                    case RESERVATION:
                        lApplicableTriggerType = ResourceTriggerApplicable.RESERVATION;
                        break;
                    case RESERVE_COMMIT:
                        lApplicableTriggerType = ResourceTriggerApplicable.RESERVE_COMMIT;
                        break;
                    case COMMIT:
                    default:
                        lApplicableTriggerType = ResourceTriggerApplicable.COMMIT;
                }
            }
            else
            {
                switch (aInResourceTriggerType)
                {
                    case RESERVATION:
                        lApplicableTriggerType =
                                ResourceTriggerApplicable.RESERVATION;
                        break;
                    case COMMIT:
                    default:
                        lApplicableTriggerType =
                                ResourceTriggerApplicable.COMMIT;
                }
            }
        }

        if (lApplicableTriggerType == ResourceTriggerApplicable.RESERVE_COMMIT)
        {
            return true;
        }
        else if (aInResourcePostProcessingContext.getResourceTriggerType() != null &&
                aInResourcePostProcessingContext.getResourceTriggerType().name() == lApplicableTriggerType.name())
        {
            return true;
        }
        return false;
    }

    /**
     * Method is used to handle All AVP levels to add CDR Tags provided in ADD-CDR Rule attribute.
     * Levels:
     * COMMAND_LEVEL
     * MSCC_LEVEL
     * CHARGING_SERVICE_LEVEL
     * RESOURCE_LEVEL
     *
     * @param aInResult    Charging Rule execution result
     * @param aInReRequest Rating engine Request Object
     */
    private static void handleAddCdrFromResultContext(ChargingPolicyDecisionResult aInResult,
            RatingEngineRequest aInReRequest)
    {
        //Handle Levels:CHARGING_SERVICE_LEVEL and RESOURCE_LEVEL
        processAddCdrFromRule(aInResult, aInReRequest.getCdrEngine());

        //Handle Levels:MSCC_LEVEL and COMMAND_LEVEL
        new AVPUtil().addAVPToCDR(aInResult, aInReRequest.getCdrEngine());
    }

    /**
     * Execute Notification action to send notifications
     *
     * @param aInResult                 Charging Rule execution result
     * @param aInREWrapper              RE Wrapper
     * @param aInResourceEntity             Resource Entity name
     * @param aInNotificationCollections Notification collection using which
     *                                   notification would be sent
     * @param aInDeviceId               Device Id
     * @param aInIgnoreSameTemplate     If notification has same template id as
     *                                  already present in notification list
     *                                  then either ignore/keep it based on this
     */
    public static void executeNotificationAction(ChargingPolicyDecisionResult
            aInResult, REWrapper aInREWrapper, String aInResourceEntity, NotificationCollectionImpl
            aInNotificationCollections, String aInDeviceId,boolean aInIgnoreSameTemplate)
    {
        if(aInREWrapper!=null&&aInREWrapper.getContextManagement()!=null)
        {
        executeNotificationAction(
                aInResult, aInREWrapper.getContextManagement()
                        .getSequenceNumberForNextNotification(),
                aInResourceEntity,
                aInNotificationCollections, aInDeviceId, aInIgnoreSameTemplate);
         }
    }

    /**
     * Execute Notification action to send notifications
     *
     * @param aInResult                 Charging Rule execution result
     * @param aInSeqNo                   aInSeqNo
     * @param aInResourceEntity             Resource Entity name
     * @param aInNotificationCollections Notification collection using which
     *                                   notification would be sent
     * @param aInDeviceId               Device Id
     * @param aInIgnoreSameTemplate     If notification has same template id as
     *                                  already present in notification list
     *                                  then either ignore/keep it based on this
     */
    public static void executeNotificationAction(ChargingPolicyDecisionResult
            aInResult, Integer aInSeqNo, String aInResourceEntity, NotificationCollectionImpl
            aInNotificationCollections, String aInDeviceId,boolean aInIgnoreSameTemplate)
    {
        List<GenericValueWrapper> lNotifications = new ArrayList<>();
        List<GenericValueWrapper> lNotificationSend = aInResult
                .getAttributeResult(ResultContextType.NOTIFICATION,
                        ChargingRuleAttributeName.SEND_NOTIFICATION);
        if (lNotificationSend == null)
        {
            TPAResources.debug(logger, "Notification send is not configured "
                    , "in rule for post processing.");
            return;
        }
        List<GenericValueWrapper> lNotificationVariables = aInResult
                .getAttributeResult(ResultContextType.NOTIFICATION,
                        ChargingRuleAttributeName.NOTIFICATION_VARIABLE);
        if (lNotificationVariables != null)
        {

            // Usage Sequence Number
            GenericValueWrapper lSequenceNumberAttr = new GenericValueWrapper();
            lSequenceNumberAttr.setName(RatingConstants.USAGE_SEQUENCE_NUMBER);
            GenericValue lGenericValue = new GenericValue(GenericValueType.STRING,
                    String.valueOf(aInSeqNo));
            GenericValue lGenericValueTemplateId = new GenericValue(GenericValueType.STRING,
                    lNotificationVariables.get(0).getValue(1).getValue());
            lSequenceNumberAttr.setValues(new GenericValue[]
                    {lGenericValue, lGenericValueTemplateId});
            TPAResources.debug(logger, "Generic value wrapper for usage sequence number is : ",
                    lSequenceNumberAttr);
            lNotifications.add(lSequenceNumberAttr);

            lNotifications.addAll(lNotificationVariables);
        }

        List<String> lNotifTempIds = new ArrayList<>();
        for(NotificationCollection.Notification lNotification :
                aInNotificationCollections.getNotifications())
        {
            lNotifTempIds.add(lNotification.getTemplateId());
        }

        Set<NotificationDetail> lNotificationSet = createSetOfNotifications(
                lNotificationSend, aInIgnoreSameTemplate, lNotifTempIds, null);

        if (aInResourceEntity != null)
        {
            TPAResources.debug(logger, "Notifications attribute for: ", aInResourceEntity);
        }

        printNotifications(lNotifications);
        //passing ThresholdRuleResultWrapper for fetching notification from processNotifications
        ThresholdRuleResultWrapper lThrRuleResultWrapper = new ThresholdRuleResultWrapper();

        List<GenericValueWrapper> lNotificationSmppSource = aInResult
                .getAttributeResult(ResultContextType.NOTIFICATION,
                        ChargingRuleAttributeName.SMPP_SOURCE);
        List<GenericValueWrapper> lNotificationSmppDestination = aInResult
                .getAttributeResult(ResultContextType.NOTIFICATION,
                        ChargingRuleAttributeName.SMPP_DESTINATION);
        Map<String, String> aInVarsMap = new HashMap<>();
        if (lNotificationSmppSource != null)
        {
            String lSmppSourceNumber = lNotificationSmppSource.get(0).getName();
            aInVarsMap.put(SPSCommonConstants.SMS_ORIG_ID, lSmppSourceNumber);
        }
        if (lNotificationSmppDestination != null)
        {
            String lSmppDestinationNumber =
                    lNotificationSmppDestination.get(0).getName();
            aInVarsMap.put(SPSCommonConstants.SMS_DEST_ID,
                    lSmppDestinationNumber);
        }

        processNotification(aInNotificationCollections,
                lNotificationSet, lNotifications, lThrRuleResultWrapper,
                aInDeviceId, Device.class.getSimpleName(), aInVarsMap);

        ThresholdExecutionUtility
                .addNotificationsToCollection(aInNotificationCollections, lThrRuleResultWrapper);
    }

    /**
     * This method will print notification variables in logs.
     *
     * @param aInNotifications Notification variables to be printed.
     */
    private static void printNotifications(List<GenericValueWrapper> aInNotifications)
    {
        if (logger.isDebugEnabled())
        {
            StringBuilder lNotificatoinsLog = new StringBuilder();
            for (GenericValueWrapper lNotifVarWrapper : aInNotifications)
            {
                if (lNotifVarWrapper.getValues() != null
                        && lNotifVarWrapper.getValues()[0]!=null)
                {
                    lNotificatoinsLog.append(lNotifVarWrapper.getName() + " = "
                            + lNotifVarWrapper.getValues()[0].getValue() + ", ");
                }
                else
                {
                    lNotificatoinsLog.append(lNotifVarWrapper.getName() + " = " + "null" + ", ");
                }
            }
            TPAResources.debug(logger, lNotificatoinsLog);
        }
    }

    /**
     * Method is used to fetch the group id to process Notification
     *
     * @param aInEntityCounterInstanceId Entity Counter Instance
     * @param aInEntityInformation Entity Information
     * @param aInAVInstance AV whose threshold actions will be updated.
     * @param aInThrRuleResult Result from rule engine.
     * @return Group Id if Bucket/Counter/AV attached with Group and destination
     *         for notification is Group
     */
    private static String getGroupId(
            EntityCounterInstanceId aInEntityCounterInstanceId,
            EntityInformation aInEntityInformation, AVInstance aInAVInstance,
            ChargingPolicyDecisionResult aInThrRuleResult)
    {
        String aOutGroupId = null;
        /**
         * Check if Entity is Bucket In entity information if entity belongs to
         * device and not to any group we set Group Id as Device, checking if
         * resource belong to device resource
         */
        if (Objects.nonNull(aInEntityInformation)
                && StringUtils.isNotBlank(aInEntityInformation.getGroupId())
                && !aInEntityInformation.getGroupId().equalsIgnoreCase(DEVICE))
        {
            aOutGroupId = aInEntityInformation.getGroupId();
        }
        // Check if Entity is Counter(handle Cap/Counter Threshold cases)
        else if (Objects.nonNull(aInEntityCounterInstanceId))
        {
            aOutGroupId = getGroupIdForCounter(aInEntityCounterInstanceId,
                    aInThrRuleResult);
        }
        // Check if Entity is AV Instance
        else if (Objects.nonNull(aInAVInstance)
                && Objects.nonNull(aInAVInstance.getGroup()))
        {
            aOutGroupId = aInAVInstance.getGroup().getId();
        }
        return aOutGroupId;
    }

    /**
     * Method is used to fetch Group Id in case of Counter
     *
     * @param aInEntityCounterInstanceId Entity Counter Instance
     * @param aInThrRuleResult Result from rule engine.
     * @return Group Id if eligible to send notification
     */
    private static String getGroupIdForCounter(
            EntityCounterInstanceId aInEntityCounterInstanceId,
            ChargingPolicyDecisionResult aInThrRuleResult)
    {
        String aOutGroupId = null;
        EntityCounterInstance aInEntityCounterInstance = Services
                .lookup(EntityCounterInstanceService.class)
                .read(aInEntityCounterInstanceId);
        if (aInEntityCounterInstance != null)
        {
            if (Objects.nonNull(aInEntityCounterInstance.getGroup())
                    && aInEntityCounterInstance.getId()
                            .getEntityCounterInstanceAttachMode()
                            .equals(EntityCounterInstanceAttachMode.DEVICE_GROUP))
            {
                NotificationDestinationType lNotificationDestination = Objects
                        .nonNull(aInThrRuleResult)
                                ? (NotificationDestinationType) aInThrRuleResult
                                        .getAttributeResult(RuleSetType.THRESHOLD,
                                                ChargingRuleAttributeName.NOTIFICATION_DESTINATION)
                                : null;
                if (Objects.nonNull(lNotificationDestination)
                        && TypeOfSubscription.GROUP.name()
                                .equals(lNotificationDestination.name()))
                {
                    aOutGroupId = aInEntityCounterInstance.getGroup().getId();
                }
            }
            else if (aInEntityCounterInstance.getId()
                    .getEntityCounterInstanceAttachMode()
                    .equals(EntityCounterInstanceAttachMode.SUBSCRIPTION))
            {
                Subscription lSubscription = Services
                        .lookup(SubscriptionService.class)
                        .read(aInEntityCounterInstanceId.getAttachModeValue());
                if (Objects.nonNull(lSubscription))
                {
                    TypeOfSubscription lSubscriptionType = lSubscription
                            .getSubscriptionType();
                    if (lSubscriptionType.equals(TypeOfSubscription.GROUP))
                    {
                        aOutGroupId = lSubscription.getGroups().get(0).getId();
                    }
                }
            }
            else if (Objects.nonNull(aInEntityCounterInstance.getGroup())
                    && (aInEntityCounterInstance.getId()
                            .getEntityCounterInstanceAttachMode()
                            .equals(EntityCounterInstanceAttachMode.GROUP)||aInEntityCounterInstance.getId()
                    .getEntityCounterInstanceAttachMode()
                    .equals(EntityCounterInstanceAttachMode.MULTI_BUNDLE)))
            {
                aOutGroupId = aInEntityCounterInstance.getGroup().getId();
            }
        }
        return aOutGroupId;
    }

    /**
     * Get Set of all first Device Id's associated with all users of account.
     *
     * @param aInAccount account object
     * @return Set of DeviceId
     */
    private static Set<String> getDeviceIdSetForAccount(Account aInAccount)

    {
        TPAResources.debug(logger,
                "RuleEngineHandler :: " , "getDeviceIdSetForAccount Start");
        Set<String> lEntityIdSet = null;
        Set<User> lAdmins = null;
        try
        {
            lAdmins = Services.lookup(UserService.class)
                    .findAdministratorsOfAccount(aInAccount.getId());

            if (lAdmins == null || lAdmins.isEmpty())
            {
                TPAResources.debug(logger,
                        "RuleEngineHandler :: "
                                , "Notification processing ends as "
                                , "Account without user exist",
                        aInAccount.getId());
                return lEntityIdSet;
            }
            DeviceService lDevicesService = Services
                    .lookup(DeviceService.class);

            lEntityIdSet = new HashSet<>();
            for (User lCurrAdmin : lAdmins)
            {
                List<Device> lDevices = null;

                lDevices = lDevicesService
                        .findDevicesByOwner(lCurrAdmin.getId());

                if ((lDevices == null) || lDevices.isEmpty())
                {
                    TPAResources.debug(logger,
                            "RuleEngineHandler :: "
                                    , "No Device Associated with User found ",
                            lCurrAdmin.getId());
                    continue;
                }
                Device lDevice = lDevices.iterator().next();
                lEntityIdSet.add(lDevice.getId());
            }
            return lEntityIdSet;
        }
        catch (OperationFailedException e)
        {
            TPAResources.logException(true, logger, Level.ERROR, e, true,
                    "TriggerRule Engine Handler :: getDeviceIdSetForAccount"
                            , ": Caught exception",
                    e.getMessage());
            if (lEntityIdSet != null && !lEntityIdSet.isEmpty())
            {
                lEntityIdSet = null;

            }
        }
        return lEntityIdSet;

    }

    /**
     * This function will return the subscriptionId.
     *
     * @param aInCounterInstanceClass Counter Instance object to get Subscription Id
     * @param aInEntityInformation    EntityInformation object to get Subscription Id
     * @return aOutSubscriptionId
     */
    public static String getSubscriptionId(CounterInstanceClass aInCounterInstanceClass,
            EntityInformation aInEntityInformation)
    {
        String aOutSubscriptionId = null;

        if (aInEntityInformation != null)
        {
            aOutSubscriptionId = aInEntityInformation.getSubInstId();
        }
        else if (aInCounterInstanceClass != null)
        {
            aOutSubscriptionId = aInCounterInstanceClass.getSubsId();
        }

        TPAResources.debug(logger, "Returning Subscription Id: [", aOutSubscriptionId, "] from getSubscriptionId.");
        return aOutSubscriptionId;
    }

    /**
     * Method is used to fetch the group id if subscription resouce is group
     * @param aInEntityCounterInstanceId Entity Counter Instance
     * @param aInEntityInformation Entity Information
     * @param aInAVInstance AV whose threshold actions will be updated.
     * @return Group Id if Bucket/Counter/AV attached with Group Subscription
     */
    public static String getGroupId(
            EntityCounterInstanceId aInEntityCounterInstanceId,
            EntityInformation aInEntityInformation,
            AVInstance aInAVInstance)
    {
        String aOutGroupId = null;
        /**
         * Check if Entity is Bucket
         * In entity information if entity belongs to device and not to
         * any group we set Group Id as Device, checking if resource
         * belong to device resource
         */
        if (Objects.nonNull(aInEntityInformation)
                && StringUtils.isNotBlank(aInEntityInformation.getGroupId())
                && !aInEntityInformation.getGroupId().equalsIgnoreCase(
                        DEVICE))
        {
            aOutGroupId = aInEntityInformation.getGroupId();
        }
        // Check if Entity is Counter(handle Cap/Counter Threshold cases)
        else if (Objects.nonNull(aInEntityCounterInstanceId))
        {
            aOutGroupId = EntityCounterUtility
                    .getGroupIdForCounter(aInEntityCounterInstanceId);
        }
        // Check if Entity is AV Instance
        else if (Objects.nonNull(aInAVInstance)
                && Objects.nonNull(aInAVInstance.getGroup()))
        {
            aOutGroupId = aInAVInstance.getGroup().getId();
        }
        return aOutGroupId;
    }
    /**
     * This method is to create GroupContext to set in SourceContext passed to
     * RuleEngine
     *
     * @param aInGroupId ID of group for which group context need to be created
     * @return GroupContext {@link GroupContext}
     */
    public static GroupContext createGroupContext(String aInGroupId)
    {
        return createGroupContext(aInGroupId, null);
    }

    /**
     * This method is to create GroupContext to set in SourceContext passed to
     * RuleEngine
     *
     * @param aInGroupId ID of group for which group context need to be created
     * @param aInGroup Group
     * * @return GroupContext {@link GroupContext}
     */
    public static GroupContext createGroupContext(String aInGroupId, Group aInGroup)
    {
        Group lGroup = null;
        if (aInGroup == null)
        {
            lGroup =
                    Objects.nonNull(aInGroupId)
                            ? ThreadLocalCallContext.getGroup(aInGroupId)
                            : null;
        }
        else {
            lGroup = aInGroup;
        }

        return Objects.nonNull(lGroup)
                ? new GroupContextImpl(lGroup)
                : null;
    }

    /**
     * This method is to create GroupContext from DeviceContext
     *
     * @param aInDeviceContext DeviceContext which comtains group info
     * @return GroupContext {@link GroupContext}
     */
    public static GroupContext createGroupContextFromDeviceContext(
            DeviceContext aInDeviceContext)
    {
        GroupContext aOutGroupContext = null;
        if (aInDeviceContext != null
                && aInDeviceContext.getGroup() != null
                && !aInDeviceContext.getGroup().isEmpty())
        {
            String lGroupId = aInDeviceContext.getGroup().get(0).getId();
            aOutGroupContext = RuleEngineHandler.createGroupContext(lGroupId);
        }
        return aOutGroupContext;
    }

    /**
     * This method is to create GroupContext using groupId present in {@link
     * EntityCounterInstance} or get GroupContext from Call if present in Call
     * Object. it will also update the Counter information in Group context, if
     * there are group counters present.
     *
     * @param aInEntityCounterInstance EntityCounterInstance
     * @param aInGroupId Group Id
     * @param aInCall                  Call object which contains GroupContext
     *                                 or not
     * @return GroupContext {@link GroupContext}
     */
    public static GroupContext createAndUpdateGroupContext(
            EntityCounterInstance aInEntityCounterInstance, String aInGroupId,
            Call aInCall)
    {
        GroupContext lGroupContext = createAndUpdateGroupContext(aInGroupId,
                aInCall);

        if (lGroupContext != null && aInEntityCounterInstance != null
                && aInEntityCounterInstance.getId() != null
                && aInEntityCounterInstance.getId()
                        .getEntityCounterInstanceAttachMode() == EntityCounterInstanceAttachMode.GROUP)
        {
            //Update CounterInstance in GroupContext.
            aInEntityCounterInstance.getCounterInstanceList()
                    .forEach((aInCounterInstanceClass) -> {
                        lGroupContext.addGroupCounters(
                                aInCounterInstanceClass.getCounterDefId()
                                , aInCounterInstanceClass);
                        long lCounterEndTime =
                                ThresholdExecutionUtility
                                        .evaluateEndDateForCounter(
                                                aInEntityCounterInstance,
                                                aInCounterInstanceClass);
                        lGroupContext.addGroupCountersEndDate(
                                aInCounterInstanceClass.getCounterDefId(),
                                lCounterEndTime);
                    });
        }

        return lGroupContext;
    }

    /**
     * This method is to create GroupContext from groupId or get GroupContext
     * from Call
     *
     * @param aInGroupId ID of group
     * @param aInCall Call which contains GroupContext or not
     * @param aInGroup Group entity
     * @return GroupContext {@link GroupContext}
     */
    public static GroupContext createAndUpdateGroupContext(String aInGroupId,
            Call aInCall, Group aInGroup)
    {
        if (aInCall.getGroupContext() != null
                && aInGroupId != null
                && aInCall.getGroupContext().getGroupId() == aInGroupId)
        {
            TPAResources.debug(logger, "Group is not changed, use the"
                    ,"same groupContext in current call.");
            return aInCall.getGroupContext();
        }
        else
        {
            TPAResources.debug(logger, "Group changed, create a new"
                    ,"groupContext and set to call.");
            GroupContext aOutGroupContext = createGroupContext(aInGroupId, aInGroup);
            if(aInCall.getGroupContext() == null && aOutGroupContext!=null)
            {
                aInCall.setGroupContext(aOutGroupContext);
            }
            return aOutGroupContext;
        }
    }

    /**
     * This method is to create GroupContext from groupId or get GroupContext
     * from Call
     *
     * @param aInGroupId ID of group
     * @param aInCall Call which contains GroupContext or not
     * @return GroupContext {@link GroupContext}
     */
    public static GroupContext createAndUpdateGroupContext(String aInGroupId,
            Call aInCall)
    {
        return createAndUpdateGroupContext(aInGroupId, aInCall, null);
    }

    /**
     * method to create the Account Context
     *
     * @param aInAccount account object
     * @return lOutAccountContext return account context
     */
    public static AccountContext createAccountContext(Account aInAccount)
    {
        AccountContext lOutAccountContext = null;
        if (Objects.nonNull(aInAccount))
        {
            populatePrevEntityLCState(aInAccount);
            populatePrevPeriodLCState(aInAccount);
            lOutAccountContext =
                    new AccountContextImpl(aInAccount);
        }
        return lOutAccountContext;
    }

    private static void populatePrevPeriodLCState(Account aInAccount) {
        if ((CommonUtil.isNullOrEmpty(aInAccount.getPrevPeriodicLCState())
                &&CommonUtil.isNotNullOrEmpty(aInAccount.getPeriodicLifeCycleState())))
            aInAccount.setPrevPeriodicLCState(aInAccount.getPeriodicLifeCycleState());
    }

    private static void populatePrevEntityLCState(Account aInAccount) {
        if ((CommonUtil.isNullOrEmpty(aInAccount.getPrevEntityLCState())
                &&CommonUtil.isNotNullOrEmpty(aInAccount.getAccountEntityLifeCycleState())))
            aInAccount.setPrevEntityLCState(aInAccount.getAccountEntityLifeCycleState());
    }

    /**
     * In some places for source context population only ReWrapper is available
     * This methods tries to get account id from rating engine request doing all the null checks
     * @param aInRatingEngineRequest rating engine request
     * @return account id if present or null
     */
    public static String getAccountIdFromRatingEngineRequest(RatingEngineRequest aInRatingEngineRequest)
    {
        String lAccountId = null;
        if (aInRatingEngineRequest != null)
        {
            if(aInRatingEngineRequest.getCall() != null &&
                    aInRatingEngineRequest.getCall().getSubscriptionContext() != null)
            {
                lAccountId = getAccountIdFromSubscriptionContext(
                        aInRatingEngineRequest.getCall().getSubscriptionContext(),
                        true);
                TPAResources.debug(logger, "AccountId from subscription=", lAccountId);
            }
            else if (aInRatingEngineRequest.getCdrEngine() != null &&
                    aInRatingEngineRequest.getCdrEngine().getCurrentLevelWiseCustomCDRs() != null &&
                    aInRatingEngineRequest.getCdrEngine().getCurrentLevelWiseCustomCDRs()
                            .getResourceCustomCDRs() instanceof MainBalanceCDR)
            {
                lAccountId = ((MainBalanceCDR) aInRatingEngineRequest.getCdrEngine()
                        .getCurrentLevelWiseCustomCDRs().getResourceCustomCDRs()).getAccountId();
                TPAResources.debug(logger, "AccountId from CDR=", lAccountId);
            }
            else
            {
                lAccountId = getAccountIdFromDevice(aInRatingEngineRequest);
            }
        }
        return lAccountId;
    }

    /**
     * Method to retrieve account id from device context
     * @param aInRatingEngineRequest    rating request
     * @return      account id
     */
    private static String getAccountIdFromDevice(
            RatingEngineRequest aInRatingEngineRequest)
    {
        String lAccountId = null;
        if(null == aInRatingEngineRequest)
        {
            TPAResources.debug(logger, "Rating request is null");
            return lAccountId;
        }

        if(CommonUtil.isNotNullOrEmpty(
                aInRatingEngineRequest.getDeviceSubscriptionList()))
        {
            lAccountId =
                    aInRatingEngineRequest.getCdrEngine().getPreferredAccount()
                            .getPreferredAccountIDOrFallback(() -> getAccountIdFromSubscriptionList(
                                    aInRatingEngineRequest.getDeviceSubscriptionList()));
            if (null != lAccountId)
            {
                return lAccountId;
            }
        }

        if(null != aInRatingEngineRequest.getCall() &&
                null != aInRatingEngineRequest.getCall().getDeviceContext() &&
                null != aInRatingEngineRequest.getCall().getDeviceContext().getDevice())
        {
            Device lDevice = aInRatingEngineRequest.getCall().getDeviceContext().getDevice();
            if(CommonUtil.isNullOrEmpty(lDevice.getSubscriptions()))
            {
                lDevice = Services.lookup(DeviceService.class).read(lDevice.getId());
            }

            if(null != lDevice && CommonUtil.isNotNullOrEmpty(lDevice.getSubscriptions()))
            {
                lAccountId = getAccountIdFromSubscriptionList(
                        lDevice.getSubscriptions());
                if(null != lAccountId)
                {
                    return lAccountId;
                }
            }
        }
        return null;
    }

    /**
     * Method to retrieve account id from subscription
     * @param aInSubscriptions      subscriptions
     * @return      account id
     */
    private static String getAccountIdFromSubscriptionList(
            List<Subscription> aInSubscriptions)
    {
        if(CommonUtil.isNotNullOrEmpty(
                aInSubscriptions))
        {
            Subscription lSubscription =
                    aInSubscriptions.get(0);
            if(null == lSubscription.getAccount() ||
                    StringUtils.isEmpty(lSubscription.getAccount().getId()))
            {
                lSubscription = getSubscription(lSubscription.getId());
            }

            if(null != lSubscription && null != lSubscription.getAccount() &&
                    StringUtils.isNotEmpty(lSubscription.getAccount().getId()))
            {
                return lSubscription.getAccount().getId();
            }
        }
        return null;
    }

    /**
     * Returns account id from subscription
     * @param aInSubscriptionContext subscription context
     * @param aInIsRawId true if requires full account id (account id with mvno and club)
     * @return account id
     */
    public static String getAccountIdFromSubscriptionContext(
            SubscriptionContext aInSubscriptionContext,
            boolean aInIsRawId)
    {
        if (aInIsRawId && aInSubscriptionContext instanceof SubscriptionContextImpl)
        {
            // Makes sure mvno is in account id to be able to read account from db
            return ((SubscriptionContextImpl) aInSubscriptionContext).getAccountIdRaw();
        }
        else
        {
            // Makes sure it won't break implementation
            // in case new types of subscrptionContext are implemented
            return aInSubscriptionContext.getAccountId();
        }
    }

    /**
     * Method to create account context from the ratingrequest object
     *
     * Usually AccountId is filled inside SubscriptionContext inside
     * RatingEngineRequest at the beginning of chargingServiceLoop in coreRatingImpl
     *
     * @param aInRatingEngineRequest - rating request object
     * @return account context or null if account not found
     */
    public static AccountContext createAccountContext(RatingEngineRequest aInRatingEngineRequest)
    {
        AccountContext lOutAccountContext = null;
        String lAccountId = getAccountIdFromRatingEngineRequest(aInRatingEngineRequest);
        TPAResources.debug(logger,
                "createAccountContext with account id = ", lAccountId);
        if (CommonUtil.isNotNullOrEmpty(lAccountId))
        {
            Account lAccount = getAccountWithBalance(lAccountId);
            lOutAccountContext = RuleEngineHandler.createAccountContext(lAccount);
        }
        return lOutAccountContext;
    }

    /**
     * Create Subscription Context
     * @param aInSubscription Subscription
     * @return Subscription context
     */
    public static SubscriptionContext createSubscriptionContext(
            Subscription aInSubscription)
    {
        SubscriptionContextImpl lOutSubscriptionContext = null;
        String lAccountId = null;

        if (aInSubscription != null)
        {
            lAccountId = aInSubscription.getAccount().getId();
        }

        lOutSubscriptionContext = new SubscriptionContextImpl(lAccountId);
        if (aInSubscription != null && !CommonUtil
                .isNullStringOrEmpty(aInSubscription.getDescription()))
        {
            TPAResources.debug(logger, "Description of Subscription ",
                    aInSubscription.getId(), " : ", aInSubscription.getDescription());
            lOutSubscriptionContext
                    .setDescription(aInSubscription.getDescription());
        }
        if (aInSubscription != null && !CommonUtil
                .isNullStringOrEmpty(aInSubscription.getId()))
        {
            lOutSubscriptionContext.setSubscriptionId(aInSubscription.getId());
        }
        return lOutSubscriptionContext;
    }

    /**
     * This method fill Sy Result Answer in case threshold attached with resource is
     * crossed
     *
     * @param aInPolicyCtrState Policy Counter State
     * @param aInRatingEngineRequest Rating Engine Request
     * @param aInEntityCounterInstanceList EntityCounterInstanceList
     * @param aInBucketDefId the bucket definition id in case of bucket type
     *                       resource.
     */

    public static void fillSyAnswerForResource(String aInPolicyCtrState,
            RatingEngineRequest aInRatingEngineRequest, List <EntityCounterInstance>
            aInEntityCounterInstanceList, String aInBucketDefId)

    {
        if (!Strings.isBlank(aInPolicyCtrState) && Objects.nonNull(aInEntityCounterInstanceList))
        {
            Map<String, String> lCountersForSNR =
                    PolicyCounterUtility.parseInputCounters(aInPolicyCtrState,
                            new SNRContext(aInBucketDefId, null));

            TPAResources.debug(logger, "Counters Configured ",
                    "for Snr in Threshold ", lCountersForSNR);

            for ( EntityCounterInstance lEntityCounterInstance : aInEntityCounterInstanceList)
            {
                if (Objects.nonNull(lCountersForSNR) &&
                        Objects.nonNull(lEntityCounterInstance.getCounterInstanceList()))
                {
                    for (String lCtrName : lCountersForSNR.keySet())
                    {
                        for (CounterInstanceClass lCounterIns :
                                lEntityCounterInstance.getCounterInstanceList())
                        {
                            if (lCounterIns.getCounterDefId()
                                    .equals(lCtrName) &&
                                    lCounterIns.isSyPolicyCounter() &&
                                    // Skip for expired counter instances
                                    lCounterIns.getDeletionTime() == null)
                            {
                                TPAResources.debug(logger,
                                        "Counter filled ",
                                        "in Sy result Answer ",
                                        lCtrName);

                                lCounterIns.setLastSNRReportingState(
                                        lCountersForSNR
                                                .get(lCtrName));
                                lCounterIns.setLastSNRReportingTime(System.
                                        currentTimeMillis());
                                String lSubsId = EntityCounterUtility.
                                        getSubscriptionId(lEntityCounterInstance.getId());
                                fillSyResultAnswerInfo(aInRatingEngineRequest,
                                        lCountersForSNR.get(lCtrName),
                                        lCounterIns, lEntityCounterInstance.getId(),
                                        false, lSubsId);
                                break;
                            }
                        }
                    }
                }
                try
                {
                    Services.lookup
                            (EntityCounterInstanceService.class).
                            updateToPolicyAttribEntityCounterInstance(lEntityCounterInstance);
                }
                catch (OperationFailedException aInE)
                {
                    TPAResources.error(logger, "Not able to ",
                            "update Entity Counter Instance in database.", aInE);
                }
            }
        }
    }

    /**
     * This method  will fetch counters corresponding to device or group
     * and trigger Snr in case threshold configured with Send - Snr Action is
     * crossed
     *
     * @param aInGroupContext Group Context
     * @param aInDeviceContext DeviceContext
     * @param aInHasNoGroup true/false
     * @param aInPolicyCtrState Policy Counter State
     * @param aInRatingEngineRequest Rating Engine Request
     * @param aInBucketDefId the bucket definition id in case of bucket type
     *                       resource.
     */
    public static void processCountersForAvOrBucket(
            GroupContext aInGroupContext, DeviceContext aInDeviceContext,
            boolean aInHasNoGroup, String aInPolicyCtrState,
            RatingEngineRequest aInRatingEngineRequest, String aInBucketDefId)
    {
        Group lGroup = null;
        Device lDevice = null;

        if (Objects.nonNull(aInGroupContext))
        {
            lGroup = Services.lookup(GroupService.class)
                    .read(aInGroupContext.getGroupId());
        }
        lDevice = Services.lookup(DeviceService.class)
                .read(aInDeviceContext.getDeviceId());
        List<EntityCounterInstance> lEntityCounterInstanceList =
                (aInHasNoGroup)
                        ? PolicyCounterUtility
                        .getDeviceSpecificPolicyCounters(lDevice)
                        : PolicyCounterUtility
                        .getGroupSpecificPolicyCounters(lGroup);

        fillSyAnswerForResource(aInPolicyCtrState, aInRatingEngineRequest,
                lEntityCounterInstanceList, aInBucketDefId);
    }

    /**
     * This method  will fetch counters corresponding to device or group
     * and trigger Snr in case threshold configured with Send - Snr Action is
     * crossed
     *
     * @param aInPolicyCtrState Policy Counter State
     * @param aInRatingEngineRequest Rating Engine Request
     * @param aInBucketDefId the bucket definition id in case of bucket type
     *                       resource.
     * @param aInBucketInstanceId the bucket instance Id.
     * @param aInSubscriptionId subscription Id
     */
    public static void processCountersForBucket(String aInPolicyCtrState,
            RatingEngineRequest aInRatingEngineRequest, String aInBucketDefId,
            String aInBucketInstanceId, String aInSubscriptionId)
    {
        if (!Strings.isBlank(aInPolicyCtrState))
        {
            Map<String, String> lCountersForSNR =
                    PolicyCounterUtility.parseInputCounters(aInPolicyCtrState,
                            new SNRContext(aInBucketDefId, aInBucketInstanceId,
                                    null, null));

            Subscription lSubscription = null;
            if(aInSubscriptionId != null)
            {
                lSubscription = getSubscription(aInSubscriptionId);
            }

            for (Map.Entry<String, String> entry : lCountersForSNR.entrySet())
            {
                TPAResources.debug(logger, "Counters Configured for Snr in",
                        " Threshold ", lCountersForSNR);

                BucketInstance lBucketInstance =
                        Services.lookup(BucketInstanceService.class).
                                readBucketInstance(aInBucketInstanceId);
                String lKey = entry.getKey();
                if (lBucketInstance.getIsSyPolicyBucket() &&
                        (lBucketInstance.getBucketDefId().equals(lKey)
                        || lKey.equals(lBucketInstance.getPolicyCounterName())))
                {
                    SignalingState lSignalingState =
                            PolicyCounterUtility.
                                    getSignalingStateAtBucketInstanceUsage(
                                            lBucketInstance, SignalingStateType.RESET);
                    String lCtrSNRState = entry.getValue();

                    if (null == lSignalingState)
                    {
                        lSignalingState = new SignalingState();
                    }
                    lSignalingState.setCurrentSignalingState(lCtrSNRState);
                    TPAResources.debug(logger, "Setting Signaling state =", lSignalingState);

                    //Update PCI info into DB
                    updateBucketPolCtrInstList(lSubscription,
                            lBucketInstance,
                            aInRatingEngineRequest.getCall().getDeviceContext()
                                    .getDevice(), lCtrSNRState);

                    fillSyResultAnswerInfoForBucketInst(aInRatingEngineRequest,
                            lSignalingState, lBucketInstance, false, lSubscription);
                    break;
                }
            }
        }
    }

    /**
     * Update Policy Counter Info of PCI into DB for bucket policy
     * counters(Virtual policy counter).
     *
     * @param aInSubscription   {@link Subscription} subscription instance.
     * @param aInBucketInstance BucketInstance of policy type.
     * @param aInDevice         {@link Device} current Device for which call was
     *                          received.
     * @param aInCurrentState   current SNR state of bucket pol counter.
     */
    private static void updateBucketPolCtrInstList(
            Subscription aInSubscription, BucketInstance aInBucketInstance,
            Device aInDevice, String aInCurrentState)
    {

        TPAResources.debug(logger, "Updating Bucket policy counter instance" ,
                " in PolicyCounterInst list: ", aInBucketInstance.getId());
        if (aInCurrentState == null)
        {
            TPAResources.debug(logger, "Current state is null, not updating" ,
                    " policy counter instance list");
            return;
        }

        try
        {
            String lPCIKey = PolicyCounterInstanceUtility
                    .getPCIKey(null, aInSubscription);
            PolicyCounterInstance lPolicyCounterInstanceDB =
                    (PolicyCounterInstance) ThreadLocalCommonContext
                            .getPolicyCtrInsMap().get(lPCIKey);
            if (lPolicyCounterInstanceDB == null)
            {
                PolicyCounterInstanceService lPolicyCounterInstanceService =
                        Services.lookup(PolicyCounterInstanceService.class);
                lPolicyCounterInstanceDB = lPolicyCounterInstanceService
                        .readPolicyCounterInstance(null, aInSubscription);
            }

            if (lPolicyCounterInstanceDB != null)
            {
                for (PolicyCounterInfo lPolicyCounterInfo : lPolicyCounterInstanceDB
                        .getCurrentPolicyCounterInfos())
                {
                    if (lPolicyCounterInfo.getBktInsId() != null
                            && lPolicyCounterInfo.getBktInsId().equals(
                                    ClubbingUtility.getBucketInstanceBusinessId(
                                            aInBucketInstance.getId())))
                    {
                        TPAResources.debug(logger,
                                "PolCounterInfo before updated : ",
                                lPolicyCounterInfo);
                        lPolicyCounterInfo.setLastSNRReportingTime(
                                ThreadLocalCommonContext
                                        .getCurrentTimeStamp());
                        /* oldSNRReportingState must not be updated when application preference
                        'Policy Counter Notification Rule' is set to PREVIOUS_STATE_VPC since old
                        states are synced for all same name VPCs/physical counters after sending
                        the SNR. Hence, old SNR reporting state from DB must be used while
                        evaluating RSV Policy Counter Status Change trigger. */
                        if (GlobalConfig.getPolicyCounterNotificationRule() ==
                                PolicyCounterNotificationRule.PREVIOUS_STATE_COUNTER)
                        {
                            if (lPolicyCounterInfo.getOldSNRReportingState() != null)
                            {
                                lPolicyCounterInfo.setOldSNRReportingState
                                        (lPolicyCounterInfo.getLastSNRReportingState());
                            }
                        }
                        lPolicyCounterInfo
                                .setLastSNRReportingState(aInCurrentState);
                        PolicyCounterInstanceUtility.updateLastSNRReportingCtrFlagInPCIObject(
                                lPolicyCounterInstanceDB,
                                null, aInBucketInstance, aInDevice);
                        TPAResources.debug(logger,
                                "Update PolCounterInfo into DB(After) : ",
                                lPolicyCounterInfo);
                        ThreadLocalCommonContext.setPolicyCtrIns(lPCIKey,
                                lPolicyCounterInstanceDB);
                        break;
                    }
                }
            }
            else
            {
                TPAResources.debug(logger, "PolCounterInstance does not exist" ,
                        " in DB to update LastSNRReportingState");
            }
        }
        catch (OperationFailedException aInException)
        {
            TPAResources.errorException(logger, aInException,
                    "Unable to update PolicyCounterInstance in DB's PolicyCounterInstaList");
        }
    }

    /**
     * Based on system property CS-SELECTION-ORDER, CounterSortingInfo
     * will be filled.
     * @param aInSubscription subscription
     * @return  expiry time
     */
    private static Long fillCreationOrExpiryTime(Subscription aInSubscription)
    {
        CSSelectionOrder lCSSelectionOrder =
                GlobalConfig.getCSSelectionOrder();
        if (lCSSelectionOrder == CSSelectionOrder.ON_CREATION_TIME)
        {
            return aInSubscription.getCreationTime();
        }
        else
        {
            return SubscriptionServiceUtil.calculateExpiryTime(
                            aInSubscription.getEndTime(),
                            aInSubscription.getValidityTime(),
                            aInSubscription.getAccount().getId());
        }
    }

    /**
     * Trigger rule for counter
     * @param aInEntityCtrInsDS Entity Counter DS
     * @param aInSourceContext Source Context
     * @return ChargingPolicyDecisionResult
     */
    public static ChargingPolicyDecisionResult triggerRuleForCounter(
            EntityCounterInstanceDS aInEntityCtrInsDS,
            Map<String, SourceContext> aInSourceContext)
    {
        final ChargingPolicyDecisionResult lChargingPolicyDecisionResult =
                new ChargingPolicyDecisionResultImpl();
        if (aInEntityCtrInsDS.getCounter() != null)
        {
            ChargingPolicyDecisionWorker.evaluateRuleTable(
                    RuleEngineConstants.RULE_TABLE_PREFIX +
                            aInEntityCtrInsDS.getCounter().getId() +
                            RULE_SUFFIX, RuleSetType.COUNTER, aInSourceContext,
                    lChargingPolicyDecisionResult);
        }

        return lChargingPolicyDecisionResult;
    }
    /**
     * method returns charging service priority
     * @param aInCsId Charging Service Id
     * @return Integer priority of CS
     */
    private static BigDecimal getChargingServicePriority(String aInCsId)
    {
        //ChargingService Lookup
        ChargingService lCSIns = Services.lookup(ChargingServiceService.
                class).read(aInCsId);
        return lCSIns != null ? lCSIns.getPriority() : null;
    }

    /**
     * method returns subscription object.
     * @param aInSubscriptionInstId subscription instance id.
     * @return subscription object.
     */
    private static Subscription getSubscription(String aInSubscriptionInstId)
    {
        String lSubBusId = ClubbingUtility.getSubscriptionBusinessId(aInSubscriptionInstId);
        return Services.lookup(SubscriptionService.class).read(lSubBusId);
    }

    /**
     * This function will create/add the group context to given SourceContext
     *
     * @param aInOutSourceContexts      SourceContext Instance
     * @param lSubscription             Subscription  Instance
     * @param aInOutRatingEngineRequest RatingEngineRequest Instance
     */
    public static void createGroupContextFromSubscription(
            Map<String, SourceContext> aInOutSourceContexts,
            Subscription lSubscription,
            RatingEngineRequest aInOutRatingEngineRequest)
    {
        TPAResources.debug(logger, "Preparing group context");

        String lGroupId = null;
        if (Objects.nonNull(lSubscription
                .getGroups()) && !lSubscription.getGroups()
                .isEmpty())
        {
            lGroupId = lSubscription
                    .getGroups().get(0).getId();
            TPAResources.debug(logger, "Subscription associated with  group ",
                    lGroupId);
        }
        else
        {
            TPAResources.debug(logger, "Subscription is not associated with ",
                    " any group ");
            return;
        }
        GroupContext lGroupContext = createAndUpdateGroupContext(lGroupId,
                aInOutRatingEngineRequest.getCall());

        if (lGroupContext != null && aInOutSourceContexts != null)
        {
            aInOutSourceContexts.put(SourceContextType.SPR_GROUP,
                    lGroupContext);
            TPAResources.debug(logger, "Group context with groupId ",
                    lGroupContext.getId(), " added to the source context");
        }
    }

    /**
     * This method is for process emergency notifications
     * in IMS call
     * @param aInDevice Device
     * @param aInDecisionResult rule execution result
     * @param aInOutNotificationCollections Notification Collection object
     * @return true if sending notification is needed, false otherwise
     */
    public static boolean checkNotificationForIMS(
            Device aInDevice,
            ChargingPolicyDecisionResult aInDecisionResult,
            NotificationCollectionImpl aInOutNotificationCollections
            )
    {

    	TPAResources.debug(logger, "check notification for ims call.");
        String lDeviceId = aInDevice.getId();
        boolean lResult = true;

        List<GenericValueWrapper> lNotifications = new ArrayList<>();
        List<GenericValueWrapper> lNotificationSend = aInDecisionResult
                .getAttributeResult(ResultContextType.NOTIFICATION,
                        ChargingRuleAttributeName.SEND_NOTIFICATION);
        if (lNotificationSend == null)
        {
            TPAResources.debug(logger, "Notification send is not configured "
                    , "in rule for ims call.");
            lResult = false;
            return lResult;
        }
        List<GenericValueWrapper> lNotificationVariables = aInDecisionResult
                .getAttributeResult(ResultContextType.NOTIFICATION,
                        ChargingRuleAttributeName.NOTIFICATION_VARIABLE);
        if (lNotificationVariables != null)
        {

            lNotifications.addAll(lNotificationVariables);
        }
        Set<NotificationDetail> lNotificationSet = createSetOfNotifications(
                lNotificationSend, false, null, null);
        printNotifications(lNotifications);

        // passing ThresholdRuleResultWrapper for fetching notifications from processNotifications
        ThresholdRuleResultWrapper lThrRuleResultWrapper = new ThresholdRuleResultWrapper();

        processNotification(aInOutNotificationCollections,
                lNotificationSet, lNotifications, lThrRuleResultWrapper, lDeviceId,
                Device.class.getSimpleName(), null);

        ThresholdExecutionUtility
                .addNotificationsToCollection(aInOutNotificationCollections, lThrRuleResultWrapper);

        return lResult;
    }

    /**
     * Process ADD-CDR attribute from rule. This method will process only CHARGING_SERVICE_LEVEL and
     * RESOURCE_LEVEL cdrs only.
     *
     * @param aInDecisionResult Charging Policy decision result.
     * @param aInCDREngine      CDR Engine
     */
    public static void processAddCdrFromRule(ChargingPolicyDecisionResult aInDecisionResult,
            CDREngine aInCDREngine)
    {

        if (!aInCDREngine.isChargingCDREnabled())
        {
            return ;
        }
        List<GenericValueWrapper> lGenericValueWrappers = getGenericValueWrappersForAddCDR(aInDecisionResult);
        if (CommonUtil.isNullOrEmpty(lGenericValueWrappers))
        {
            return;
        }

        Set<AVPLevel> lAvpLevels = new HashSet<>();

        for (GenericValueWrapper lCDRTag : lGenericValueWrappers)
        {
            if (Objects.isNull(lCDRTag.getName()) ||
                    Objects.isNull(lCDRTag.getValue(0)) ||
                    Objects.isNull(lCDRTag.getValue(1)))
            {
                TPAResources.debug(logger, "Mandatory values are missing ", lCDRTag);
                continue;
            }
            //add-CDR(#TagName#, #TagValue#, #AVPLevel#, #TAGAction#)
            String lAttributeName = lCDRTag.getName();
            String lAttributeValue = lCDRTag.getValue(0).getValue();
            String lAttributeLevel = lCDRTag.getValue(1).getValue();
            //Remove ENUM from Stating Ex. AVP_LEVEL.CHARGING_SERVICE_LEVEL
            lAttributeLevel = StringUtils.substringAfter(lAttributeLevel,
                    RatingConstants.DOT_STRING);
            AVPLevel lAVPLevel = AVPLevel.valueOf(lAttributeLevel);

            if ((lAVPLevel != AVPLevel.CHARGING_SERVICE_LEVEL) && (lAVPLevel != AVPLevel.RESOURCE_LEVEL) && (lAVPLevel != AVPLevel.SERVICE_LEVEL))
            {
                continue;
            }
            //Optional Param TAGAction
            TAGAction lTAGAction = getCdrAction(lCDRTag.getValue(2));
            lAvpLevels.add(lAVPLevel);

            //Set Data into Custom Tags recordSet to process at Last
            aInCDREngine.addRecordData(lAttributeName, lAttributeValue, lAVPLevel, lTAGAction);

        }

        //Reset Context after Processing Of RuleSet
        if (aInCDREngine.getCurrentLevelWiseCustomCDRs() != null && aInCDREngine.getCurrentMsccCDR().
            getDeviceInfoCDR().getSubscriptionCDRList() != null && !aInCDREngine.getCurrentMsccCDR().
            getDeviceInfoCDR().getSubscriptionCDRList().isEmpty()) {
            for(AVPLevel lAVPLevel : lAvpLevels){
                aInCDREngine.copyCustomTagsFromContext(lAVPLevel);
            }
            aInCDREngine.getCurrentLevelWiseCustomCDRs().clearCustomCdrContext();
        }
    }

    /**
     * Method return the tax id from inside the resource CDR inside the wiselevelCDR inside the
     * CDREngine
     */
    public static String getTaxIdIfApplicable( String lAttributeValue, CDREngine aInCDREngine)
    {
     if (lAttributeValue != null && lAttributeValue.equals("Get-Tax-Id-From-Applied-Rate") &&
          !(aInCDREngine.getCurrentLevelWiseCustomCDRs().getResourceCustomCDRs() instanceof MainBalanceCDR) &&
                (aInCDREngine.getCurrentLevelWiseCustomCDRs().getResourceCustomCDRs() instanceof BucketCDR))
            {
               BucketCDR lBucketCDR =(BucketCDR)aInCDREngine.getCurrentLevelWiseCustomCDRs().getResourceCustomCDRs();
               if ( !lBucketCDR.getBucketKindOfUnit().equals("MONEY"))
               {

                   lAttributeValue = Services.lookup(RateService.class).read(lBucketCDR.getRateId()).getTaxId();
               }
           }
      return lAttributeValue;
    }

    /**
     * Method is used to add custom field in session provided in Add-CustomField-In-Session Rule attribute.
     * Levels:
     * ROOT
     * RG
     *
     * @param aInDecisionResult    Charging Rule execution result
     * @param aInReRequest Rating engine Request Object
     */
    public static void handleAddCustomFieldFromResultContext(ChargingPolicyDecisionResult aInDecisionResult,
                                                     RatingEngineRequest aInReRequest)
    {
        // GenericValueConverter Instance for converting GenericValueWrapper
        GenericValueConverter lValueConverter =
                RuleEnginePluginCache.getInstance(ChargingPolicyConstants.CHARGING_PDF_APPLICATION).getConverter();

        List<CustomField> lCustomList = aInReRequest.getCustomData();
        List<GenericValueWrapper> lGenericValueWrappers = getGenericValueWrappersForAddCustomField(aInDecisionResult);
        if (CommonUtil.isNullOrEmpty(lGenericValueWrappers))
        {
            return;
        }

        for (GenericValueWrapper lCustomField : lGenericValueWrappers)
        {
            if (Objects.isNull(lCustomField.getName()) ||
                    Objects.isNull(lCustomField.getValue(0)) ||
                    Objects.isNull(lCustomField.getValue(1)))
            {
                TPAResources.debug(logger, "Mandatory values are missing ", lCustomField);
                continue;
            }
            //Add-CustomField-In-Session(#Key#, #Value#, #CustomFieldLevel#, #SaveValueOfResource#)
            String lAttributeName = lCustomField.getName();
            String lAttributeValue = lCustomField.getValue(0).getValue();

            // Fetching CustomFieldLevel from Rule: Add-CustomField-In-Session
            CustomFieldLevel lCustomLevel =
                    (CustomFieldLevel) lValueConverter.extractFromGeneric(lCustomField.getValue(1));

            //Set Data into Custom Field
            CustomField lCustom = new CustomField();
            Map<String, String> lCustomDataMap = new HashMap<>();

            lCustomDataMap.put(lAttributeName, lAttributeValue);
            lCustom.setCustomField(lCustomDataMap);
            lCustom.setCustomLevel(lCustomLevel);

            // Add SaveValueOfResource in CustomField Object
            if (lCustomField.getValues().length >= 3 && lCustomField.getValue(2) != null)
            {
                SaveValueOfResource lSaveValueOfResource =
                        (SaveValueOfResource) lValueConverter.extractFromGeneric(lCustomField.getValue(2));
                if (Objects.nonNull(lSaveValueOfResource))
                {
                    lCustom.setSaveValueOfResource(lSaveValueOfResource);
                }
            }

            if (lCustomList == null)
            {
                lCustomList = new ArrayList<>();
            }
            lCustomList.add(lCustom);
        }
        aInReRequest.setCustomData(lCustomList);
    }

    /**
     * This method will parse CDR action argument of ADD-CDR rule attribute and will return as
     * enum.
     *
     * @param aInGenericValue   Generic Value Object of CDR-action.
     * @return                  TAG Action
     */
    public static TAGAction getCdrAction(GenericValue aInGenericValue)
    {
        TAGAction lOutTAGAction = null;
        if (Objects.nonNull(aInGenericValue))
        {
            String lAction = aInGenericValue.getValue();
            if(aInGenericValue.getType() == GenericValueType.ENUM)
            {
                lAction = StringUtils.substringAfter(lAction, RatingConstants.DOT_STRING);
                lOutTAGAction = TAGAction.valueOf(lAction);
            }
        }
        return lOutTAGAction;
    }

    /**
     * Method is used to extract List of Add-CDR from Rule Result
     *
     * @param aInDecisionResult : Rule result
     * @return List of Add-CDR
     */
    private static List<GenericValueWrapper> getGenericValueWrappersForAddCDR(
            ChargingPolicyDecisionResult aInDecisionResult)
    {
        List<GenericValueWrapper> lOutGenericValueWrappers = null;
        if (Objects.nonNull(aInDecisionResult) && Objects.nonNull(aInDecisionResult.getResult()) &&
                Objects.nonNull(aInDecisionResult.getResult().get(ResultContextType.CALL_RESULT)))
        {
            Map<String, Object> lCallResultMap = aInDecisionResult.getResult().get(ResultContextType.CALL_RESULT);
            lOutGenericValueWrappers = (List<GenericValueWrapper>) lCallResultMap
                    .get(ChargingRuleAttributeName.ADD_AVP_IN_CDR.value());
        }
        TPAResources.debug(logger, "Rule Output for Add-CDR: " , lOutGenericValueWrappers);
        return lOutGenericValueWrappers;

    }

    /**
     * gather Copy-CDR-Tag rules and add in CDREngine object.
     *
     * @param aInDecisionResult Charging Policy decision result.
     * @param aInCDREngine      CDR Engine
     */
    public static void gatherCopyCdrTagRules(ChargingPolicyDecisionResult aInDecisionResult,
            CDREngine aInCDREngine)
    {
        if (!aInCDREngine.isChargingCDREnabled())
        {
            return;
        }
        List<GenericValueWrapper> lGenericValueWrappers =
                aInDecisionResult.getAttributeResult(ResultContextType.CALL_RESULT,
                        ChargingRuleAttributeName.COPY_CDR_TAG);
        if (CommonUtil.isNullOrEmpty(lGenericValueWrappers))
        {
            return;
        }

        for (GenericValueWrapper lCDRTag : lGenericValueWrappers)
        {
            keepCopyCdrRuleInCdrEngine(aInCDREngine, lCDRTag);
        }
    }

    private static void keepCopyCdrRuleInCdrEngine(
            CDREngine aInCDREngine, GenericValueWrapper lCDRTag)
    {
        String lDestinationAttributeName = lCDRTag.getName();
        String lSourceAttributeName = getGenericValueOrEmpty(lCDRTag.getValue(1));

        boolean lValuesAreCorrect = true;
        if (StringUtils.isBlank(lDestinationAttributeName) ||
                StringUtils.isBlank(lSourceAttributeName))
        {
            TPAResources.debug(logger, "Tag name was not provided ", lCDRTag);
            lValuesAreCorrect = false;
        }
        AVPLevel lDestinationAVPLevel = null;
        TAGLevel lSourceTAGLevel = null;
        try
        {
            lDestinationAVPLevel = AVPLevel.valueOf(
                    NotificationHandler.removeEnumPart(getGenericValueOrEmpty(lCDRTag.getValue(0))));
            lSourceTAGLevel = TAGLevel.valueOf(
                    NotificationHandler.removeEnumPart(getGenericValueOrEmpty(lCDRTag.getValue(2))));
        }
        catch (IllegalArgumentException aInEx)
        {
            TPAResources.debug(logger, "Wrong enum value ", lCDRTag, " Exception:", aInEx);
            lValuesAreCorrect = false;
        }
        AbstractCustomCDR lLevelwiseCdr = null;
        if (AVPLevel.RESOURCE_LEVEL == lDestinationAVPLevel)
        {
            lLevelwiseCdr =
                    aInCDREngine.getCurrentLevelWiseCustomCDRs().getResourceCustomCDRs();
            if (lLevelwiseCdr == null)
            {
                lValuesAreCorrect = false;
            }
        }
        if (!lValuesAreCorrect)
        {
            return;
        }

        AggregationType lAggregationType = AggregationType.UNIQUE;
        try
        {
            lAggregationType = AggregationType.valueOf(
                    NotificationHandler.removeEnumPart(getGenericValueOrEmpty(lCDRTag.getValue(3))));
        }
        catch (IllegalArgumentException aInEx)
        {
            TPAResources
                    .debug(logger, "Wrong AggregationType enum value ", lCDRTag.getValue(3),
                            " Defaulting to AggregationType.UNIQUE",
                            " Exception:", aInEx);
        }
        aInCDREngine.addCopyTagRule(
                new CopyCdrTagRuleElements(lDestinationAVPLevel, lDestinationAttributeName,
                        lSourceTAGLevel, lSourceAttributeName, lAggregationType, lLevelwiseCdr)
        );
    }

    private static String getGenericValueOrEmpty(GenericValue aInValue)
    {
        if (Objects.isNull(aInValue))
        {
            return "";
        }
        return aInValue.getValue();
    }


    /**
     * Method is used to extract List of Add-CustomField-In-Session from Rule Result
     *
     * @param aInDecisionResult : Rule result
     * @return List of Add-CustomField-In-Session
     */
    private static List<GenericValueWrapper> getGenericValueWrappersForAddCustomField(
            ChargingPolicyDecisionResult aInDecisionResult)
    {
        List<GenericValueWrapper> lOutGenericValueWrappers = null;
        if (Objects.nonNull(aInDecisionResult) && Objects.nonNull(aInDecisionResult.getResult()) &&
                Objects.nonNull(aInDecisionResult.getResult().get(ResultContextType.SESSION_PARAMETERS)))
        {
            Map<String, Object> lCallResultMap = aInDecisionResult.getResult().get(ResultContextType.SESSION_PARAMETERS);
            lOutGenericValueWrappers = (List<GenericValueWrapper>) lCallResultMap
                    .get(ChargingRuleAttributeName.ADD_CUSTOM_FIELD.value());
        }
        TPAResources.debug(logger, "Rule Output for Add-CustomField-In-Session: " , lOutGenericValueWrappers);
        return lOutGenericValueWrappers;
    }

    /**
     * convert supplied timestamp into ZonedDateTime using the supplied TimeZoneId
     *
     * @param aInTimeZoneId : time zone id
     * @param aInTimeStamp  : timestamp
     * @return : ZonedDateTime
     */
    private static ZonedDateTime getReqZonedDateTime(String aInTimeZoneId,
            long aInTimeStamp)
    {
        TimeValueGenerator lTimeValueGenerator = new TimeValueGenerator();
        return lTimeValueGenerator.transformTz(aInTimeStamp, aInTimeZoneId);
    }

    /**
     * method to set values from rate context in ruleEngineResult
     *
     * @param aInRateContext      RateContext with rate information
     * @param aInRuleEngineResult RuleEngineResult to be populated with rule Result
     */
    private static void fillRuleEngineResultWithRateContext(RateContext aInRateContext,
            RuleEngineResult aInRuleEngineResult)
    {
        // Taking rate from rate context and set in rule engine result.
        aInRuleEngineResult.setRateEntityKey(aInRateContext.getRateId());
        // Taking Tax from rate context and set in rule engine result.
        aInRuleEngineResult.setTaxEntityKey(aInRateContext.getTaxId());
        TPAResources.debug(logger, "CostBucketEnhancement: Rate: ",
                aInRateContext.getRateId(), " from rate context is set in Rule engine result.");
        if (null != aInRateContext.getOverRidingCost())
        {
            aInRuleEngineResult.setExternalCostProvided(true);
            aInRuleEngineResult.setOverridingCost(aInRateContext.getOverRidingCost());
            TPAResources.debug(logger, "CostBucketEnhancement: Overriding cost: ",
                    aInRateContext.getOverRidingCost(), " from rate context is set in Rule engine result.");
        }
    }

    /**
     * method to check whether tariff rule include threshold rate
     * if there is, save related info in rule engine result
     * @param aInResult      tariff rule result
     * @param aInOutRuleEngineResult RuleEngineResult
     * @param aInThresholdRate means for threshold rate or threshold next rate
     */
    private static void setThresholdRateResult(
            ChargingPolicyDecisionResult aInResult,
            RuleEngineResult aInOutRuleEngineResult,
            boolean aInThresholdRate)
    {
        List<GenericValueWrapper> lThresholdRateResult;
        if (aInThresholdRate)
        {
            TPAResources.debug(logger, "begin check threshold rate");
            lThresholdRateResult = aInResult.getAttributeResult(
                    ResultContextType.RATING, ChargingRuleAttributeName.THRESHOLD_RATE);
        }
        else
        {
            TPAResources.debug(logger, "begin check threshold next rate");
            lThresholdRateResult = aInResult.getAttributeResult(
                    ResultContextType.RATING, ChargingRuleAttributeName.THRESHOLD_NEXT_RATE);

        }

        if (lThresholdRateResult == null)
        {
            TPAResources.debug(logger, "no threshold rate or next rate");
            return;
        }

        Object[] lRateInfo =
                getRateInfoMap(lThresholdRateResult, false);
        String lCounterId = String.valueOf(lRateInfo[0]);
        HashMap<String, List<String>> lthresholdRateInfo =
                (HashMap<String, List<String>>) lRateInfo[1];

        if (lthresholdRateInfo.size() > 0)
        {
            aInOutRuleEngineResult.setThresholdRateCounterId(lCounterId);
            aInOutRuleEngineResult.setThresholdRateInfo(lthresholdRateInfo);
            if (aInThresholdRate)
            {
                TPAResources.debug(logger, "threshold rate counterId: ", lCounterId,
                        " threshold rate info: ", lthresholdRateInfo);
            }
            else
            {
                TPAResources.debug(logger, "threshold next rate counterId: ", lCounterId,
                        " threshold next rate info: ", lthresholdRateInfo);
            }
        }
    }

    /*
     * method to prepare the rateInfoMap for thresholdRate and tieredRate
     * @param aInCommonRateResult either the thresholdRateResult or tieredRateResult
     * @param aInIsTieredConfig true if Tiered Rate or Next Rate
     */
    private static Object[] getRateInfoMap(
            List<GenericValueWrapper> aInCommonRateResult,
            boolean aInIsTieredConfig)
    {
        //will create either thresholdRateInfo or tieredRateInfo based on the arguement received
        HashMap<String ,List<String>> lCommonRateInfo =  new HashMap<String, List<String>>();
        GenericValueConverter lValueConverter = RuleEnginePluginCache.getInstance(
                ChargingPolicyConstants.CHARGING_PDF_APPLICATION).getConverter();

        String lRateId = null;
        String lCounterId = null;
        String lTempCounterId = null;
        String lTempThreshold = null;
        String lTempTarget = null;
        for (GenericValueWrapper lRateInfo : aInCommonRateResult)
        {
            //index 0: rateId, index 1: target
            List<String> lRate = new ArrayList<String>();

            lRateId = lRateInfo.getName();

            if (lRateInfo.getValues() != null && lValueConverter != null)
            {
                lTempCounterId = (String) lValueConverter.extractFromGeneric(
                        lRateInfo.getValues()[0], false);
                lTempThreshold = (String) lValueConverter.extractFromGeneric(
                        lRateInfo.getValues()[1], false);
                lTempTarget = (String) lValueConverter.extractFromGeneric(
                        lRateInfo.getValues()[2], false);
                if(lCounterId == null)
                {
                    lCounterId = lTempCounterId;
                }
                else if (!lCounterId.equals(lTempCounterId))
                {
                    continue;
                }
                lRate.add(lRateId);
                if (lTempTarget != null)
                {
                    lRate.add(lTempTarget);
                }
            }
            if (lRate.size() > 0)
            {
                if (aInIsTieredConfig && StringUtils.isEmpty(lTempThreshold))
                {
                    TPAResources.debug(logger,
                            "Setting the empty/null threshold name as ",
                            DEFAULT_TIER,
                            " to save as key in map for Tiered Rate");
                    lTempThreshold = DEFAULT_TIER;
                }
                lCommonRateInfo.put(lTempThreshold, lRate);
            }
        }
        TPAResources.debug(logger, "Counter Id: ", lCounterId, " RateInfo: ",
                lCommonRateInfo);
        return new Object[] {lCounterId, lCommonRateInfo};
    }

    /**
     * method to prepare the rateInfoMap for tieredRate
     *
     * @param aInCommonRateResult either the tieredRateResult
     * @param aInIsTieredNextRate boolean variable. true if Tiered Secondary
     *                            rate to consider
     * @return Array containing couterId, tierRateinfo map, tieredRate object
     */
    public static Object[] getTieredRateInfoMap(
            List<GenericValueWrapper> aInCommonRateResult,
            boolean aInIsTieredNextRate)
    {
        //will create tieredRateInfo
        HashMap<String, List<String>> lCommonRateInfo = new HashMap<>();
        GenericValueConverter lValueConverter =
                RuleEnginePluginCache.getInstance(
                        ChargingPolicyConstants.CHARGING_PDF_APPLICATION)
                        .getConverter();
        if (null == lValueConverter ||
                CommonUtil.isNullOrEmpty(aInCommonRateResult))
        {
            return new Object[]{};
        }

        String lTieredRateId = null;
        String lCounterId = null;
        GenericValueWrapper lRateInfo = aInCommonRateResult.get(0);

        lTieredRateId = lRateInfo.getName();
        lCounterId = lTieredRateId;

        TieredRate lTieredRate =
                Services.lookup(TieredRateService.class).read(lTieredRateId);

        if (null == lTieredRate ||
                CommonUtil.isNullOrEmpty(lTieredRate.getTiers()))
        {
            return new Object[]{};
        }
        String lTempTarget = (String) lValueConverter.extractFromGeneric(
                lRateInfo.getValue(), false);
        String lTempTier = null;
        for (Tier lTier : lTieredRate.getTiers())
        {
            //index 0: rateId, index 1: target
            List<String> lRate = new ArrayList<>();
            if (aInIsTieredNextRate)
            {
                lRate.add(lTieredRate.getId() + UNDERSCORE + lTier.getName() +
                        SECONDARY);
            }
            else
            {
                lRate.add(lTieredRate.getId() + UNDERSCORE + lTier.getName() +
                        PRIMARY);
            }
            if (StringUtils.isNotEmpty(lTempTarget))
            {
                lRate.add(lTempTarget);
            }
            if (StringUtils.isEmpty(lTempTier))
            {
                TPAResources.debug(logger, SPSCommonConstants.TIERED_RATE,
                        lRateInfo.getName(),
                        " is default tier. Setting its name as ",
                        DEFAULT_TIER,
                        " to save as key in map for Tiered Rate");
                lTempTier = DEFAULT_TIER;
            }
            else
            {
                lTempTier = lTier.getName();
            }
            lCommonRateInfo.put(lTempTier, lRate);
        }
        TPAResources.debug(logger, SPSCommonConstants.TIERED_RATE,
                "Counter Id: ", lCounterId,
                " RateInfo: ", lCommonRateInfo,
                " lTieredRate:", lTieredRate);
        return new Object[]{lCounterId, lCommonRateInfo, lTieredRate};
    }


    /**
     * method to check whether tariff rule include tiered rate if there is, save
     * related info in rule engine result
     *
     * @param aInResult              tariff rule result
     * @param aInOutRuleEngineResult RuleEngineResult
     * @param aInIsTieredNextRate    boolean variable. true if Tiered Secondary
     *                               rate to consider
     * @return List<String> which contains rateId and targetValue for zero
     * threshold applied on Tiered Rate (default tier)
     */
    private static List<String> setTieredRateResult(
            ChargingPolicyDecisionResult aInResult,
            RuleEngineResult aInOutRuleEngineResult,
            boolean aInIsTieredNextRate)
    {
        List<GenericValueWrapper> lTieredRateResult;
        if (aInIsTieredNextRate)
        {
            TPAResources.debug(logger, SPSCommonConstants.TIERED_RATE,
                    "begin check tiered Next rate");
        }
        else
        {
            TPAResources.debug(logger, SPSCommonConstants.TIERED_RATE,
                    "begin check tiered rate");
        }
        lTieredRateResult = aInResult.getAttributeResult(
                ResultContextType.RATING, ChargingRuleAttributeName.TIERED_RATE);

        if (lTieredRateResult == null)
        {
            TPAResources.debug(logger, SPSCommonConstants.TIERED_RATE,
                    "no tiered rate");
            return Collections.emptyList();
        }

        // lRateInfoArr:  index 0-> counterId, index 1->rateInfoMap, index 2->tieredRate object
        Object[] lRateInfoArr =
                getTieredRateInfoMap(lTieredRateResult, aInIsTieredNextRate);
        if(CommonUtil.isNullOrEmpty(lRateInfoArr) || 3 < lRateInfoArr.length)
        {
            TPAResources.debug(logger, SPSCommonConstants.TIERED_RATE,
                    "Invalid tiered-rate info", lRateInfoArr);
            return Collections.emptyList();
        }
        String lCounterId = String.valueOf(lRateInfoArr[0]);
        // lTieredRateInfoMap: key-> tierName, value->Array of rateId, target
        HashMap<String, List<String>> lTieredRateInfoMap =
                (HashMap<String, List<String>>) lRateInfoArr[1];

        if (lTieredRateInfoMap.size() > 0)
        {
            TPAResources.debug(logger, SPSCommonConstants.TIERED_RATE,
                    "tiered rate counterId: ", lCounterId,
                    " tiered rate info: ", lTieredRateInfoMap);

            List<String> lDefaultRateInfolist = null;
            if(lTieredRateInfoMap.containsKey(DEFAULT_TIER))
            {
                lDefaultRateInfolist = lTieredRateInfoMap.get(DEFAULT_TIER);
                TPAResources.debug(logger, SPSCommonConstants.TIERED_RATE,
                        "Default Rate Info for Tiered Rate: ",
                        lDefaultRateInfolist);
                lTieredRateInfoMap.remove(DEFAULT_TIER);
                TPAResources.debug(logger, SPSCommonConstants.TIERED_RATE,
                        "Remove this default Rate info from Map to avoid checking new rate based on crossed threshold as this rate will already be saved in ctxMngmnt");
            }

            if(aInIsTieredNextRate)
            {
                RuleEngineResult lNextRateRuleEngineResult =
                        initialiseNxtRateRuleResltObj();
                lNextRateRuleEngineResult.setTieredRateCounterId(lCounterId);
                lNextRateRuleEngineResult.setTieredRateInfo(lTieredRateInfoMap);
                lNextRateRuleEngineResult.setTieredRate((TieredRate) lRateInfoArr[2]);
                aInOutRuleEngineResult
                        .setNextRateRuleEngineResult(lNextRateRuleEngineResult);
            }
            else
            {
                aInOutRuleEngineResult.setTieredRateCounterId(lCounterId);
                aInOutRuleEngineResult.setTieredRateInfo(lTieredRateInfoMap);
                aInOutRuleEngineResult.setTieredRate((TieredRate) lRateInfoArr[2]);
            }
            return lDefaultRateInfolist;
        }
        return Collections.emptyList();
    }

    /**
     * Method to get BucketInstance from CS that matches the bucket from rule
     *
     * @param aInCsInst Charging Service Instance.
     * @param aInBucketName bucket name from rule
     * @param aInOutReWrapper RuleResultWrapper object
     * @return BucketInstanceId
     */
    private static String getNxtBucketInstance(
            ChargingServiceInstance aInCsInst, String aInBucketName,
            RuleResultWrapper aInOutReWrapper)
    {
        String lNextBucketInstance = getBucketInstId(aInCsInst, aInBucketName);
        if (lNextBucketInstance == null)
        {
            TPAResources.debug(logger, "Bucket Instance for bucket " , "Def: ",
                    aInBucketName, " not attached with any CS ");
            aInOutReWrapper.setDefaultAction(RuleEngineEnum.RULE_ENGINE_ERROR);
            return null;
        }
        return lNextBucketInstance;
    }

    /**
     * Method to initialise Next rateReuleResult Object
     *
     * @return RuleEngineResult
     */
    private static RuleEngineResult initialiseNxtRateRuleResltObj()
    {
        RuleEngineResult lNextRateRuleEngineResult = new RuleEngineResult();
        lNextRateRuleEngineResult.setIsNextRateRuleEngineResult(true);
        return lNextRateRuleEngineResult;
    }
    /**
     * This method triggers Device life cycle event, if Trigger-Life-Cycle-Event
     * attribute is configured with GY_PRE_PROCESSING in RSV, matches with the
     * DEVICE lifecycle event on current device
     *
     * @param aInLifeCycleTriggerEvents List<GenericValueWrapper>
     * @param aInDevice                 Device
     */
    public static void triggerRuleEngineForGyPreProcessing(
            List<GenericValueWrapper> aInLifeCycleTriggerEvents, Device aInDevice)
    {
        // Create EntityIdModel from input Entity
        EntityIdModel lEntityIdModel =
                new EntityIdModel(null,
                        null != aInDevice ? aInDevice.getDeviceNameId() :
                                null, null,
                        null, null);

        for (GenericValueWrapper lWrapper : aInLifeCycleTriggerEvents)
        {
            String lTriggerEventName = lWrapper.getName();
            if (StringUtils.isBlank(lTriggerEventName))
            {
                TPAResources.debug(logger, SKIP_TRIGGER_EVENT,
                        IS_NULL_EMPTY);
                continue;
            }
            TriggerLifeCycleEntityTypeAsDevice lEntityTypeInRSV =
                    (TriggerLifeCycleEntityTypeAsDevice) getConverter()
                            .extractFromGeneric(lWrapper.getValue());

            TriggerLifeCycleEntityType lEntityType = null;
            if (null != lEntityTypeInRSV && lEntityTypeInRSV.toString()
                    .equals(TriggerLifeCycleEntityType.DEVICE.toString()))
            {
                lEntityType = TriggerLifeCycleEntityType.DEVICE;
            }
            else{
                return;
            }

            // Triggering life cycle event
            triggerLifeCycleEvent(lTriggerEventName, lEntityIdModel,
                    lEntityType);
        }

    }

    /**
     * This method triggers life cycle event if Trigger-Life-Cycle-Event action
     * is configured on RESOURCE_POST_PROCESSING
     *
     * @param aInLifeCycleTriggerEvents List<GenericValueWrapper>
     * @param aInDeviceContext          DeviceContext
     * @param aInGroupContext           GroupContext
     * @param aInOwnerId                owner Id
     * @param aInAccountId              account Id
     * @param subscriptionId            subscription Id
     */
    public static void triggerRuleEngineForResourcePostProcessing(
            List<GenericValueWrapper> aInLifeCycleTriggerEvents,String subscriptionId, DeviceContext aInDeviceContext,
            GroupContext aInGroupContext, String aInOwnerId,
            String aInAccountId)
    {
        // Create EntityIdModel from input Entity
        EntityIdModel lEntityIdModel =
                new EntityIdModel(subscriptionId,
                        aInDeviceContext.getDeviceId(), aInAccountId,
                        aInOwnerId,
                        Objects.isNull(aInGroupContext) ? null :
                                aInGroupContext.getGroupId());

        for (GenericValueWrapper lWrapper : aInLifeCycleTriggerEvents)
        {
            String lTriggerEventName = lWrapper.getName();
            if (StringUtils.isBlank(lTriggerEventName))
            {
                TPAResources.debug(logger, SKIP_TRIGGER_EVENT,
                        IS_NULL_EMPTY);
                continue;
            }
            TriggerLifeCycleEntityType lTriggerLifeCycleEntityType =
                    (TriggerLifeCycleEntityType) getConverter()
                            .extractFromGeneric(lWrapper.getValue());

            if (lTriggerLifeCycleEntityType ==
                    TriggerLifeCycleEntityType.SUBSCRIPTION &&
                    null != aInDeviceContext.getDevice())
            {

                // Triggering life cycle event for Subscription Entity
                for (Subscription lSubscription : aInDeviceContext.getDevice()
                        .getSubscriptions())
                {
                    lEntityIdModel =
                            new EntityIdModel(lSubscription.getId(),
                                    null != aInDeviceContext ?
                                            aInDeviceContext.getDeviceId() :
                                            null, aInAccountId, aInOwnerId,
                                    Objects.isNull(aInGroupContext) ? null :
                                            aInGroupContext.getGroupId());

                    triggerLifeCycleEvent(lTriggerEventName, lEntityIdModel,
                            lTriggerLifeCycleEntityType);
                }
            }
            else
            {
                // Triggering life cycle event for other than Subscription Entities
                triggerLifeCycleEvent(lTriggerEventName, lEntityIdModel,
                        lTriggerLifeCycleEntityType);
            }
        }

    }

    /**
     * insert counter related information in VSA announcement object,
     * these information is used for announcement frequency handling
     * @param aInReAnswer
     * @param aInEntityCounterInstance
     * @param aInCounterInstanceClass
     * @param aInThrshldPrfContext
     * @param aInThrRuleResultWrapper
     */
    private static void prepareCounterInfoForAnnouncement(
            RatingEngineAnswer aInReAnswer,
            EntityCounterInstance aInEntityCounterInstance,
            CounterInstanceClass aInCounterInstanceClass,
            ThresholdProfileContextImpl aInThrshldPrfContext,
            ThresholdRuleResultWrapper aInThrRuleResultWrapper)
    {
        String lRuleTableName = aInThrshldPrfContext.getProfileRuleTableName();
        if (aInEntityCounterInstance == null ||
                aInCounterInstanceClass == null )
        {
            TPAResources.debug(logger, "skipped for non counter");
            return;
        }
        TPAResources.debug(logger, "counter ID: ",
                aInCounterInstanceClass.getCounterDefId());
        List<ThresholdRuleEngineResult> lResultList = aInThrRuleResultWrapper.
                getThresholdRuleEngineResultList();
        if (CommonUtil.isNullOrEmpty(lResultList))
        {
            TPAResources.debug(logger, "skipped as no threshold crossed");
            return;
        }
        List<AnnouncementDetail> lAnnouncementInfoList = aInReAnswer.
                getAnnouncementDetailList();
        if (CommonUtil.isNullOrEmpty(lAnnouncementInfoList))
        {
            TPAResources.debug(logger, "skipped as no announcement");
            return;
        }
        String lLastThresholdName = null;
        String lThresholdName = null;
        Double lLastThresholdValue = Double.MIN_VALUE;
        Double lThresholdValue;
        for (ThresholdRuleEngineResult lThresholdRuleEngineResult : lResultList)
        {
            lThresholdName = lThresholdRuleEngineResult.getThresholdName();
            lThresholdValue = aInThrshldPrfContext.getThresholdValue(lThresholdName);
            if (lThresholdValue.compareTo(lLastThresholdValue) > 0 )
            {
                lLastThresholdValue = lThresholdValue;
                lLastThresholdName = lThresholdName;
            }
        }
        for (AnnouncementDetail lAnnouncementInfo : lAnnouncementInfoList)
        {
            if (lAnnouncementInfo.getRuleTableName() != null &&
                    lAnnouncementInfo.getRuleTableName().equals(lRuleTableName))
            {
                lAnnouncementInfo.setEntityCounterInstanceId(
                        aInEntityCounterInstance.getId());
                lAnnouncementInfo.setCounterId(
                        aInCounterInstanceClass.getCounterDefId());
                lAnnouncementInfo.setCounterInsId(
                        aInCounterInstanceClass.getCounterInsId());
                lAnnouncementInfo.setThresholdProfileGroupName(
                        aInThrshldPrfContext.getThresholdProfileGroupName());
                lAnnouncementInfo.setThresholdName(lLastThresholdName);
                lAnnouncementInfo.setRuleSetType(RuleSetType.THRESHOLD);
                TPAResources.debug(logger, "set counter info for threshold name: ",
                        lLastThresholdName);
            }
        }
    }

    /**
     * save the 3gpp announcement result from threshold rule for
     * later priority and frequency check
     *
     * @param aInReAnswer  rating engine answer
     * @param aInEntityCounterInstance entity counter instance
     * @param aInCounterInstanceClass counter instance
     * @param aInThrshldPrfContext threshold profile context
     * @param aInThrRuleResultWrapper threshold rule result
     * @param aInChargingPolicyDecisionResult policy decision result
     * @param aInDevice device
     */
    private static void handleStandardAnnouncementResult(
            RatingEngineAnswer aInReAnswer,
            EntityCounterInstance aInEntityCounterInstance,
            CounterInstanceClass aInCounterInstanceClass,
            ThresholdProfileContextImpl aInThrshldPrfContext,
            ThresholdRuleResultWrapper aInThrRuleResultWrapper,
            ChargingPolicyDecisionResult aInChargingPolicyDecisionResult,
            Device aInDevice)
    {
        if (aInChargingPolicyDecisionResult.getContextResults(
                ResultContextType.CALL_RESULT) == null)
        {
            TPAResources.debug(logger, "skipped for no result action");
            return;
        }
        AVPUtil lAVPUtil = new AVPUtil();
        Set<AnnouncementInformation> lAnnouncementInformations = lAVPUtil.getAnnouncementAVP(
                aInChargingPolicyDecisionResult, aInDevice);
        if (CommonUtil.isNullOrEmpty(lAnnouncementInformations))
        {
            TPAResources.debug(logger, "skipped as no announcement");
            return;
        }

        if (aInEntityCounterInstance == null ||
                aInCounterInstanceClass == null )
        {
            TPAResources.debug(logger, "skipped for non counter");
            return;
        }
        TPAResources.debug(logger, "counter ID: ",
                aInCounterInstanceClass.getCounterDefId());
        List<ThresholdRuleEngineResult> lResultList = aInThrRuleResultWrapper.
                getThresholdRuleEngineResultList();
        if (CommonUtil.isNullOrEmpty(lResultList))
        {
            TPAResources.debug(logger, "skipped as no threshold crossed");
            return;
        }

        GenericValueConverter lConverter = RuleEnginePluginCache
                .getInstance(ChargingPolicyConstants.CHARGING_PDF_APPLICATION)
                .getConverter();
        GenericValueWrapper lAnnouncementPara = aInChargingPolicyDecisionResult
                .getAttributeResult(ResultContextType.ANNOUNCEMENT,
                ChargingRuleAttributeName.ANNOUNCEMENT_PARAMETER);
        AnnouncementDetail lAnnouncementDetail = new AnnouncementDetail();
        if(lAnnouncementPara != null && lAnnouncementPara.getValue(0) != null)
        {
            lAnnouncementDetail.setAnnouncementFrequency(
                    ((BigInteger) (lConverter.extractFromGeneric(
                    lAnnouncementPara.getValue(0), false)))
                    .longValue());
        }
        if(lAnnouncementPara != null && lAnnouncementPara.getValue(1) != null)
        {
            lAnnouncementDetail.setAnnouncementPriority(
                    ((BigInteger) (lConverter.extractFromGeneric(
                    lAnnouncementPara.getValue(1), false)))
                    .longValue());
        }
        lAnnouncementDetail.setStandardAnnouncements(lAnnouncementInformations);
        TPAResources.debug(logger, "Announcement Frequency is : "
                , lAnnouncementDetail.getAnnouncementFrequency());
        TPAResources.debug(logger, "Announcement Priority is : "
                , lAnnouncementDetail.getAnnouncementPriority());

        lAnnouncementDetail.setRuleTableName(
                aInThrshldPrfContext.getProfileRuleTableName());
        aInReAnswer.getAnnouncementDetailList().add(lAnnouncementDetail);
    }

    /**
     * set the reporting reason metrics specified in the rule result
     * if the message include the corresponding reporting reason
     * @param aInREWrapper rating engine request wrapper
     * @param aInResult rule result
     */
    private static void handleReportingReasonMetricsResult(
            REWrapper aInREWrapper,
            ChargingPolicyDecisionResult aInResult)
    {
        RatingEngineRequest lReRequest = aInREWrapper.getRatingEngineRequest();
        List<ReportingReason> lReportingReasons = aInResult.getAttributeResult(
                ResultContextType.CALL_RESULT,
                ChargingRuleAttributeName.REPORTING_REASON_IN_METRICS);
        TPAResources.debug(logger,"reporting reason metrics from rule:",
                lReportingReasons);
        if (lReportingReasons == null)
        {
            return;
        }
        GyMessageContext lGyMessageContext = lReRequest.getCall().
                getGyMessageContext();
        if (lGyMessageContext == null)
        {
            return;
        }
        Set<ReportingReason> lReportReasonsInRequest =
                lGyMessageContext.getMSCCReportingReasonList();
        if (CommonUtil.isNullOrEmpty(lReportReasonsInRequest))
        {
            TPAResources.debug(logger,"no reporting reason in message");
            return;
        }
        MultipleServicesCreditControl lMscc = lGyMessageContext.
                getCurrentMscc();
        String lServiceContextId = lGyMessageContext.getServiceContextId();
        MSCCKey lMSCCKey = new MSCCKey(lServiceContextId,
                lMscc.getRatingGroup(), lMscc.getServiceIdentifier());
        for (ReportingReason lReason : lReportingReasons)
        {
            if (!lReportReasonsInRequest.contains(lReason))
            {
                TPAResources.debug(logger,"invalid reporting reason metrics:", lReason.name());
                continue;
            }
            TPAResources.debug(logger,"valid reporting reason metrics:", lReason.name());
            switch (lReason)
            {
                case THRESHOLD:
                    lReRequest.getCdrEngine().addMetricsAttribute(lMSCCKey,
                            RatingMetricsAttributes.REPORTING_REASON_THRESHOLD,
                            lReason.name());
                    break;
                case QHT:
                    lReRequest.getCdrEngine().addMetricsAttribute(lMSCCKey,
                            RatingMetricsAttributes.REPORTING_REASON_QHT,
                            lReason.name());
                    break;
                case FINAL:
                    lReRequest.getCdrEngine().addMetricsAttribute(lMSCCKey,
                            RatingMetricsAttributes.REPORTING_REASON_FINAL,
                            lReason.name());
                    break;
                case QUOTA_EXHAUSTED:
                    lReRequest.getCdrEngine().addMetricsAttribute(lMSCCKey,
                            RatingMetricsAttributes.REPORTING_REASON_QUOTA_EXH,
                            lReason.name());
                    break;
                case VALIDITY_TIME:
                    lReRequest.getCdrEngine().addMetricsAttribute(lMSCCKey,
                            RatingMetricsAttributes.REPORTING_REASON_VALIDITY_TIME,
                            lReason.name());
                    break;
                case OTHER_QUOTA_TYPE:
                    lReRequest.getCdrEngine().addMetricsAttribute(lMSCCKey,
                            RatingMetricsAttributes.REPORTING_REASON_OTHER_QUOTA,
                            lReason.name());
                    break;
                case RATING_CONDITION_CHANGE:
                    lReRequest.getCdrEngine().addMetricsAttribute(lMSCCKey,
                            RatingMetricsAttributes.REPORTING_REASON_RATING_COND_CHG,
                            lReason.name());
                    break;
                case FORCED_REAUTHORISATION:
                    lReRequest.getCdrEngine().addMetricsAttribute(lMSCCKey,
                            RatingMetricsAttributes.REPORTING_REASON_FORCED_REAUTH,
                            lReason.name());
                    break;
                case POOL_EXHAUSTED:
                    lReRequest.getCdrEngine().addMetricsAttribute(lMSCCKey,
                            RatingMetricsAttributes.REPORTING_REASON_POOL_EXH,
                            lReason.name());
                    break;
                default:
                    break;
            }
        }
    }

    /**
     * This is a method to handle adding metrics from rules
     * @param aInREWrapper rating engine request wrapper
     * @param aInResult rule result
     */
    public static void handleAddingMetricsAttribute(
            REWrapper aInREWrapper,
            ChargingPolicyDecisionResult aInResult)
    {
        if(null == aInREWrapper || null == aInREWrapper.getRatingEngineRequest())
        {
            TPAResources.debug(logger,"Rating engine request is null");
            return;
        }
        RatingEngineRequest lReRequest = aInREWrapper.getRatingEngineRequest();
        if(null==aInResult)
        {
            TPAResources.debug(logger,"Charging Policy Decision Result is null");
            return;
        }
        List<GenericValueWrapper> lGenerateMetric = aInResult
                .getAttributeResult(ResultContextType.RATING,
                        ChargingRuleAttributeName.ADD_METRICS_ATTRIBUTE);
        TPAResources.debug(logger,"Getting Attribute Results",
                lGenerateMetric);
        if (CommonUtil.isNullOrEmpty(lGenerateMetric))
        {
            TPAResources.debug(logger,"no Rules found for GenerateMetric," ,
                    "GenericValueWrapper list is null or empty");
            return;
        }
        if (lReRequest.getCall() == null || lReRequest.getCall().getGyMessageContext() == null)
        {
            TPAResources.debug(logger,"no gy message context can be found," ,
                    "call context is either null or empty");
            return;
        }
        GyMessageContext lGyMessageContext = lReRequest.getCall().
                getGyMessageContext();
        MultipleServicesCreditControl lMscc = lGyMessageContext.
                getCurrentMscc();
        String lServiceContextId = lGyMessageContext.getServiceContextId();
        MSCCKey lMSCCKey = new MSCCKey(lServiceContextId,
                lMscc.getRatingGroup(), lMscc.getServiceIdentifier());
        for (GenericValueWrapper genericValueWrapper : lGenerateMetric)
        {
            String metricName= genericValueWrapper.getName();
            GenericValue genericValue= genericValueWrapper.getValue();
            if(null == genericValue)
            {
                continue;
            }
            String metricValue= genericValue.getValue();
            TPAResources.debug(logger,"getting metric name=",metricName,
                    ", value=",metricValue," from a rule");
            if(StringUtils.isNotBlank(metricName) && StringUtils.isNotBlank(metricValue))
            {
                lReRequest.getCdrEngine().addMetricsAttribute(lMSCCKey,
                    metricName, metricValue);
                TPAResources.debug(logger,"Metric with name=",metricName,
                        ", value=",metricValue," was added using cdrEngine");
            }
        }
    }

    /**
     * Set used service units corresponding to single MSCC reported in Update/Terminate Request
     * @param aInCommonCallContext Common CallContext
     * @param aInSyResultAnswer Sy Result Answer
     */
    private static void fillUsageInSyResultAnswer (CommonCallContext aInCommonCallContext,
                                                    SyResultAnswer aInSyResultAnswer)
    {
        if(Objects.nonNull(aInCommonCallContext))
        {
            boolean isMonetaryUSU = aInCommonCallContext.getKindOfUnit() ==
                    KindOfUnit.MONEY ? true : false;

            RequestType lRequestType = aInCommonCallContext.getRequestType();
            if ((lRequestType == RequestType.UPDATE_REQUEST ||
                    lRequestType == RequestType.TERMINATE_REQUEST)
                    && Objects.nonNull(aInCommonCallContext.getUsedServiceUnits()))
            {
                aInSyResultAnswer.setUsageFromLastCCR(
                        NumberUtility.convertLongToBigDecimal(
                                aInCommonCallContext.getUsedServiceUnits(),
                                isMonetaryUSU));
            }
        }
    }
      /**
     * This method check Entity Information for Subscription,Device,Group,Multi-Bundle type Counter
     * @param aInContextManagement ContextManagement
     * @param aInPostProcessingContext PostProcessingContextImpl
     * @return counter is Applicable or not
     */
    private static boolean isCounterApplicableForNoCharge(
            ContextManagement aInContextManagement,
            PostProcessingContextImpl aInPostProcessingContext)
    {
        for(EntityInformation lEntityInformation: aInContextManagement.getListEntityInformation())
        {
            if (lEntityInformation.getEntityType().equals(EntityTypeEnum.NOCHARGE) &&
                    aInPostProcessingContext.getChargingService().equals(lEntityInformation.getCsDef()))
            {
                if (!CommonUtil.isNullOrEmpty(lEntityInformation.getCsCounterIds()) ||
                        !CommonUtil.isNullOrEmpty(lEntityInformation.getMultiBundleCounterIds()))
                {
                    return true;
                }
                else if (!CommonUtil.isNullOrEmpty(
                        aInContextManagement.getEntityCounterInformationMap()))
                {
                   return aInContextManagement.getEntityCounterInformationMap().
                            entrySet().stream().anyMatch(lEntryEntityCounter->lEntryEntityCounter.
                            getKey().getEntityCounterInstanceAttachMode().equals(
                                    EntityCounterInstanceAttachMode.DEVICE)
                            || lEntryEntityCounter.getKey().getEntityCounterInstanceAttachMode().
                            equals(EntityCounterInstanceAttachMode.GROUP)
                            || lEntryEntityCounter.getKey().getEntityCounterInstanceAttachMode().
                           equals(EntityCounterInstanceAttachMode.DEVICE_GROUP));
                }
            }
        }
        return false;
    }

    /**
     * method to create the Account Context
     *
     * @param aInAccountId account object
     * @return return AccountContext object
     */
    public static AccountContext createAccountContextFromAccountId(String aInAccountId)
    {
        return createAccountContextFromAccountId(aInAccountId, null);
    }
    /**
     * method to create the Account Context
     *
     * @param aInAccountId account object
     * @param aInAccount   account entity
     * @return return AccountContext object
     */
    public static AccountContext createAccountContextFromAccountId(String aInAccountId,
                                                                   Account aInAccount)
    {
        TPAResources.debug(logger,
                "createAccountContextFromAccountId = ", aInAccountId);
        AccountContext lOutAccountContext = null;
        if (CommonUtil.isNotNullOrEmpty(aInAccount))
        {
            lOutAccountContext = createAccountContext(aInAccount);
        }
        else if (CommonUtil.isNotNullOrEmpty(aInAccountId))
        {
            Account lAccount = getAccountWithBalance(aInAccountId);
            lOutAccountContext = createAccountContext(lAccount);
        }
        return lOutAccountContext;
    }

    /**
     * method to create the Account Context
     *
     * @param aInAccount account object
     * @param aInSubscriptionChargingServiceDS List of Secondary Bundle Charging Service
     * @return return AccountContext object
     */
    private static AccountContext createAccountContextFromAccountIdWithSecondaryBalance(String aInAccount,
                                                                                        List<SubscriptionChargingServiceDS> aInSubscriptionChargingServiceDS)
    {
        AccountContextImpl aOutAccountContext = (AccountContextImpl) createAccountContextFromAccountId(aInAccount);

        if (aOutAccountContext!=null)
        {
            aOutAccountContext.setSubscriptionChargingServiceDSs(aInSubscriptionChargingServiceDS);
        }

        return aOutAccountContext;
    }

    /**
     * Method to get account with its balance
     * @param aInAccountId AccountId
     * @return Account Object or null if not found
     */
    public static Account getAccountWithBalance(String aInAccountId)
    {
        TPAResources.debug(logger,
                "Enter getAccountWithBalance for AccountId=", aInAccountId);
        Account lOutAccount =
                Services.lookup(AccountService.class).read(aInAccountId);
        TPAResources.debug(logger,
                "Read account =", lOutAccount);
        // If something fails reading the account or balance
        // Continues normal execution with account context null
        if (lOutAccount != null)
        {
            try
            {
                AccountBalance lAccountBalance = Services
                        .lookup(AccountService.class)
                        .getBalance(aInAccountId);
                TPAResources.debug(logger, "Read account balance = ",
                        lAccountBalance);
                lOutAccount.setBalance(lAccountBalance);
            }
            catch (OperationFailedException aInE)
            {
                TPAResources.logException(true, logger, Level.ERROR, aInE, true,
                        "RuleEngineHandler :: createAccountContextFromAccountId : Caught exception",
                        aInE.getMessage());
            }
        }
        return lOutAccount;
    }

    /**
     * Used to find an account id during post processing rule source context creation
     * @param aInReRequest request wrapper
     * @param aInPostProcessingContext post processing context
     * @param aInSubscriptionContext subscription context
     * @return AccountId
     */
    public static String findAccountIdForPostTrigger(RatingEngineRequest aInReRequest,
            PostProcessingContextImpl aInPostProcessingContext,
            SubscriptionContext aInSubscriptionContext)
    {
        String lOutAccountId = getAccountIdFromRatingEngineRequest(aInReRequest);
        if (CommonUtil.isNullOrEmpty(lOutAccountId) &&
                aInPostProcessingContext != null)
        {
            lOutAccountId = aInPostProcessingContext.getBillingAccId();
        }
        if (CommonUtil.isNullOrEmpty(lOutAccountId) && aInSubscriptionContext != null)
        {
            lOutAccountId = aInSubscriptionContext.getAccountId();
        }
        TPAResources.debug(logger, "Returning account id = ", lOutAccountId);
        return lOutAccountId;
    }

    /**
     * Used in to find the account id to be used to create account context during Threshold trigger population
     * @param aInAccount an account object
     * @param aInSubscriptionContext subscription context
     * @param aInEntityCounterInstance counter instance
     * @return account id string or null
     */
    public static String findAccountIdForThresholdTrigger(Account aInAccount,
            SubscriptionContext aInSubscriptionContext,
            EntityCounterInstance aInEntityCounterInstance)
    {
        String lOutAccountId = null;
        if (aInAccount != null && CommonUtil.isNotNullOrEmpty(aInAccount.getId()))
        {
            lOutAccountId = aInAccount.getId();
        }
        else if (aInSubscriptionContext != null &&
                CommonUtil.isNotNullOrEmpty(aInSubscriptionContext.getAccountId()))
        {
            lOutAccountId = getAccountIdFromSubscriptionContext(
                    aInSubscriptionContext, true);
        }
        else if (aInEntityCounterInstance != null &&
                aInEntityCounterInstance.getAccount() != null &&
                CommonUtil.isNotNullOrEmpty(aInEntityCounterInstance.getAccount().getId()))
        {
            lOutAccountId = aInEntityCounterInstance.getAccount().getId();
        }
        TPAResources.debug(logger, "Returning account id = ", lOutAccountId);
        return lOutAccountId;
    }

    /**
     * This method is used to handle Wholesale Rate and NextRate in RSV RESOURCE_POST_PROCESSING trigger
     *
     * @param aInREWrapper   Rating request object received from call control
     * @param aInResult    Charging Rule execution result
     */
    private static void handleTriggerWholesaleActionForResourcePostProcessingFromResultContext(
            REWrapper aInREWrapper, ChargingPolicyDecisionResult aInResult, ResourcePostProcessingContext aInResourcePostProcessingContext)
    {
        TPAResources.debug(logger, "Wholesale Enabled set to Selective with commit trigger type for Session "
                , aInREWrapper.getContextManagement().getSessionId());

        List<GenericValueWrapper> lWholesaleWrapper = aInResult
                .getAttributeResult(ResultContextType.RATING,
                        ChargingRuleAttributeName.APPLY_SELECTIVE_WHOLESALE);

        if (lWholesaleWrapper != null && !lWholesaleWrapper.isEmpty()
                && !CommonUtil.isNullOrEmpty(lWholesaleWrapper.get(0)))
        {
            aInREWrapper.getRatingEngineRequest().setProfessionalBucketExists(true);
            if (aInResourcePostProcessingContext.getResourceTriggerType().equals(ResourceTriggerType.COMMIT) &&
                    WholesaleEnabledValues.Disabled != GlobalConfig.getWholesaleEnabled()
                    && (!aInREWrapper.getRatingEngineRequest().getCall().getCommonCallContext().getRequestType().equals(RequestType.EVENT_REQUEST) ||
                    aInREWrapper.getRatingEngineRequest().getWholesaleRate() == null))
            {
                String lWholesaleRate = lWholesaleWrapper.get(0).getName();
                String lWholesaleNextRate = null;
                if(null != lWholesaleWrapper.get(0).getValue())
                {
                    lWholesaleNextRate = lWholesaleWrapper.get(0).getValue().getValue();
                }
                aInREWrapper.getRatingEngineRequest().setWholesaleRate(lWholesaleRate);
                aInREWrapper.getRatingEngineRequest().getCall().getCommonCallContext().setWholesaleSimulationRate(lWholesaleRate);
                TPAResources.debug(logger, "Professional bucket found for session "
                        , aInREWrapper.getContextManagement().getSessionId());
                TPAResources.debug(logger, "Setting Wholesale Rate : " , lWholesaleRate ,
                        " and Next Rate : " ,lWholesaleNextRate ,
                        " in Rating engine Request for session : "
                        , aInREWrapper.getContextManagement().getSessionId());
                if(lWholesaleNextRate!=null && !lWholesaleNextRate.isEmpty())
                {
                    aInREWrapper.getRatingEngineRequest().setWholesaleNextRate(lWholesaleNextRate);
                    aInREWrapper.getRatingEngineRequest().getCall().getCommonCallContext().setWholesaleSimulationNextRate(lWholesaleNextRate);
                }
            }
        }else{
            aInREWrapper.getRatingEngineRequest().setProfessionalBucketExists(false);
            aInREWrapper.getRatingEngineRequest().getCall().getCommonCallContext().setWholesaleSimulationRate(null);
            aInREWrapper.getRatingEngineRequest().setWholesaleCLExists(false);
        }
    }


    public static void adjustBucketForTerminationRequest(
            ContextManagement contextManagement, String buktid)
            throws OperationFailedException
    {
        if (buktid != null && contextManagement.getUsedServiceUnits() != null &&
                !contextManagement.getUsedServiceUnits().isEmpty() &&
                !contextManagement.getGroupSubscriptions().isEmpty())
        {
            REUnitType bucketUnitType = Services.lookup(
                    BucketInstanceServiceImpl.class).read(buktid).getUnitType();
            if (bucketUnitType.getKindOfUnit().equals(KindOfUnit.VOLUME) &&
                    bucketUnitType.getId().equalsIgnoreCase(contextManagement
                            .getUsedServiceUnits()
                            .get(0).getSubType()))
            {
                Services.lookup(
                        BucketInstanceServiceImpl.class)
                        .adjustBalance(buktid,
                                contextManagement
                                        .getUsedServiceUnits()
                                        .get(0).getUnitValue()
                                        .negate(),
                                contextManagement.isGoToDirectDebit());

            }

        }
    }

    /**
     * process General Result Action for Subscriber Entity
     *
     * @param aInGeneralAttributeMap general result from rsv
     * @param aInDevice              Reference Device
     */
    public static void processGeneralResultAction(
            Map<ChargingRuleAttributeName, Object> aInGeneralAttributeMap,
            Device aInDevice)
    {
        TPAResources.debug(logger, "Enter processGeneralResultAction method");
        if (CommonUtil.isNotNullOrEmpty(aInGeneralAttributeMap))
        {
            for (Map.Entry<ChargingRuleAttributeName, Object> lEntry : aInGeneralAttributeMap
                    .entrySet())
            {
                if (CommonUtil.isNullOrEmpty(lEntry.getValue()))
                {
                    continue;
                }
                if (lEntry.getKey() ==
                        ChargingRuleAttributeName.TRIGGER_LIFE_CYCLE_EVENT)
                {
                    handleTriggerLifeCycleEvent(lEntry.getValue(), aInDevice);
                }
                else
                {
                    TPAResources.debug(logger,
                            "no result action to process ",
                            "founded in SubscriberProvision");
                }
            }
        }
        TPAResources.debug(logger,
                "Exit from processGeneralResultAction method");
    }

    /**
     * method to handle Trigger LifeCycle Event
     *
     * @param aInValue  List of GenericValueWrapper
     * @param aInDevice Reference Device
     */
    private static void handleTriggerLifeCycleEvent(Object aInValue,
            Device aInDevice)
    {
        if (CommonUtil.isNotNullOrEmpty(aInValue) && aInValue instanceof List)
        {
            List<GenericValueWrapper> lLifeCycleTriggerEvents =
                    (List<GenericValueWrapper>) aInValue;
            // Create EntityIdModel from input Entity
            EntityIdModel lEntityIdModel =
                    new EntityIdModel(null,
                            null != aInDevice ? aInDevice.getDeviceNameId() :
                                    null, null,
                            null, null);

            for (GenericValueWrapper lWrapper : lLifeCycleTriggerEvents)
            {
                String lTriggerEventName = lWrapper.getName();
                if (StringUtils.isBlank(lTriggerEventName))
                {
                    TPAResources.debug(logger, SKIP_TRIGGER_EVENT,
                            IS_NULL_EMPTY);
                    continue;
                }

                TriggerLifeCycleEntityTypeAsDevice lEntityTypeInRSV =
                        (TriggerLifeCycleEntityTypeAsDevice) getConverter()
                                .extractFromGeneric(lWrapper.getValue());

                if (null != lEntityTypeInRSV && lEntityTypeInRSV.toString()
                        .equals(TriggerLifeCycleEntityType.DEVICE.toString()))
                {
                    BeanContext.lookup(LifeCycleTrigger.class).triggerLifeCycleEventOnDevice(lEntityIdModel.getDeviceId(), lTriggerEventName);
                }
                else
                {
                    return;
                }
            }
        }
    }

    /**
     * Method to extract attribute values from result-context 'Tax-Selection', if present in Rules
     * @param aInRuleEngineResult : Output from Rule
     * @return : Pair of tax-profile name and tax-type
     */
    private static Pair<String, TaxType> getTaxSelectionAttributesFromRuleResult(
            ChargingPolicyDecisionResult aInRuleEngineResult)
    {
        GenericValueWrapper lTaxSelectionAttributes =
                aInRuleEngineResult.getAttributeResult(ResultContextType.RATING,
                        ChargingRuleAttributeName.TAX_SELECTION);

        String lTaxName = null;
        TaxType lTaxType = TaxType.ON_TOP_OF_COST;

        if (lTaxSelectionAttributes != null)
        {
            lTaxName = lTaxSelectionAttributes.getName();

            GenericValueConverter lValueConverter = RuleEnginePluginCache.getInstance(
                    ChargingPolicyConstants.CHARGING_PDF_APPLICATION).getConverter();

            if (lValueConverter != null && lTaxSelectionAttributes.getValue()!= null)
            {
                lTaxType = (TaxType) lValueConverter
                        .extractFromGeneric(lTaxSelectionAttributes.getValue(), false);
            }
        }
        return new Pair<>(lTaxName, lTaxType);
    }

    public static void handleLoanActivationDuringVoiceCall(ChargingPolicyDecisionResult aInChargingPolicyDecisionResult, Device aInDevice)
    {
        // get all Rules in the trigger
        Map<String, Map<String, Object>> lResultMap =
                aInChargingPolicyDecisionResult.getResult();
        if (CommonUtil.isNullOrEmpty(lResultMap))
        {
            return;
        }

        // get CALL_RESULT context rules
        Map<String, Object> lGyResultMap =
                lResultMap.get(ResultContextType.CALL_RESULT);

        // check if CALL_RESULT context containes LOAN_ACTIVATION rule
        if (CommonUtil.isNullOrEmpty(lGyResultMap) ||
                !lGyResultMap.containsKey(
                        ChargingRuleAttributeName.LOAN_ACTIVATION.value()))
        {
            return;
        }

        // get the value of the loan Id from LOAN_ACTIVATION rule
        GenericValueWrapper lLoanIdValueWrapper =(GenericValueWrapper) lGyResultMap.get(
                ChargingRuleAttributeName.LOAN_ACTIVATION.value());

        String lLoanIdFromCallResultRule = lLoanIdValueWrapper.getName();

        TPAResources.debug(logger,
                "Loan Id from Loan-Activation Attribute is :",
                lLoanIdFromCallResultRule +
                        (CommonUtil.isNullOrEmpty(lLoanIdFromCallResultRule) ?
                                "and it will be taken from loan ID " +
                                        "defined in LOAN_PRE_PROCESSING and LOAN_POST_PROCESSING rule"
                                : ""));

        // extract the device identity
        DeviceIdentity lDeviceIdentity =
                aInDevice.getIdentities().stream().findFirst().orElse(null);
        if (CommonUtil.isNullOrEmpty(lDeviceIdentity))
        {
            TPAResources.debug(logger,
                    "Device identities not found for :" + aInDevice);
            return;
        }

        //initialize Loan Request Object
        LoanRequest lLoanRequest = new LoanRequest();
        lLoanRequest.setLoanID(lLoanIdFromCallResultRule);
        lLoanRequest.setOperation(LoanOperation.OPT_IN);
        lLoanRequest.setDeviceType(lDeviceIdentity.getIdentityType());
        lLoanRequest.setDeviceValue(lDeviceIdentity.getValue());

        TPAResources.debug(logger, "Loan Request Object is :"+ lLoanRequest);

        // trigger the loanRequest Service
        LoanRequestServiceImpl lLoanRequestService = new LoanRequestServiceImpl();
        try
        {
            lLoanRequestService.processLoanRequest(lLoanRequest);
        }
        catch (EntityFinalException | AlignBundleException |
               EntityBarredException | OperationFailedException  aInE)
        {
            TPAResources.logException(true, logger, Level.ERROR, aInE, true,
                    "RuleEngineHandler :: handleLoanActivationDuringVoiceCall : Caught exception",
                    aInE.getMessage());
        }

    }

    /**
     * Process RSV Event
     *
     * @param aInSourceContextMap          source context required to evaluate
     *                                     result
     * @param aInChargingRuleAttributeName Rule Attribute names in the charging
     * @param aInRuleSetType               rule set type
     * @param aInResultContextType         type of result context
     * @param aInChargingPolicyDecisionResult         rule result
     */
    public static void processEvent(
            Map<String, SourceContext> aInSourceContextMap,
            ChargingRuleAttributeName aInChargingRuleAttributeName,
            String aInRuleSetType, String aInResultContextType,
            ChargingPolicyDecisionResult aInChargingPolicyDecisionResult)
    {
        if (aInChargingPolicyDecisionResult == null || aInChargingPolicyDecisionResult.getResult() == null)
        {
            aInChargingPolicyDecisionResult =
                    new ChargingPolicyDecisionResultImpl();
            ChargingPolicyDecisionWorker
                    .evaluate(aInRuleSetType,
                            aInSourceContextMap, aInChargingPolicyDecisionResult,
                            null);
        }
        GyMessageContext lGyMessageContext =
                (GyMessageContext) aInSourceContextMap
                        .get(SourceContextType.GY_MESSAGE);

        AccountContext lAccountContext =
                (AccountContext) aInSourceContextMap
                        .get(SourceContextType.SPR_ACCOUNT);

        DeviceContext lDeviceContext =
                (DeviceContext) aInSourceContextMap
                        .get(SourceContextType.SPR_DEVICE);

        UserContext lUserContext =
                (UserContext) aInSourceContextMap
                        .get(SourceContextType.SPR_USER);

        GroupContext lGroupContext = (GroupContext) aInSourceContextMap
                .get(SourceContextType.SPR_GROUP);

        // Evaluate the trigger
        Map<ChargingRuleAttributeName, Object> lGeneralAttributeMap =
                getGeneralResultContext(aInChargingPolicyDecisionResult,
                        aInChargingRuleAttributeName,
                        aInResultContextType);

        EntityIdModel lEntityIdModel =
                new EntityIdModel(getSubscriptionId(lGyMessageContext),
                        getDeviceId(lDeviceContext),
                        getAccountId(lAccountContext),
                        getUserId(lUserContext),
                        getGroupId(lGroupContext));

        processGeneralResultAction(lGeneralAttributeMap,
                aInChargingRuleAttributeName, lEntityIdModel);
    }

    /**
     * get General Result Context
     *
     * @param aInResult                 ChargingPolicyDecisionResult instance
     * @param chargingRuleAttributeName Rule Attribute names in the charging
     * @param resultContextType         Result Context Type
     * @return map of General Result Context
     */
    private static Map<ChargingRuleAttributeName, Object> getGeneralResultContext(
            ChargingPolicyDecisionResult aInResult,
            ChargingRuleAttributeName aInChargingRuleAttributeName,
            String aInResultContextType)
    {
        TPAResources.debug(logger, "Enter getGeneralResultContext method");
        Map<ChargingRuleAttributeName, String> lContextMap =
                new LinkedHashMap<>();
        Map<ChargingRuleAttributeName, Object> aOutAttrMap =
                new LinkedHashMap<>();
        lContextMap.put(aInChargingRuleAttributeName,
                aInResultContextType);
        for (Entry<ChargingRuleAttributeName, String> lEntry : lContextMap
                .entrySet())
        {
            Object lResult = aInResult
                    .getAttributeResult(lEntry.getValue(), lEntry.getKey());
            if (isNotNullOrEmpty(lResult))
            {
                aOutAttrMap.put(lEntry.getKey(), lResult);
                TPAResources.debug(logger, "Charging Attribute Name : ",
                        lEntry.getKey(), " , and Result is : ",
                        aOutAttrMap.get(lEntry.getKey()));
            }
        }
        TPAResources.debug(logger, "Exit from getGeneralResultContext method");
        return aOutAttrMap;
    }

    /**
     * process General Result Action for Entity
     *
     * @param aInGeneralAttributeMap       general result from rsv
     * @param aInChargingRuleAttributeName Rule Attribute names in the charging
     * @param aInEntityIdModel             EntityIdModel reference
     */
    private static void processGeneralResultAction(
            Map<ChargingRuleAttributeName, Object> aInGeneralAttributeMap,
            ChargingRuleAttributeName aInChargingRuleAttributeName,
            EntityIdModel aInEntityIdModel)
    {
        TPAResources.debug(logger, "Enter processGeneralResultAction method");
        if (isNotNullOrEmpty(aInGeneralAttributeMap))
        {
            for (Entry<ChargingRuleAttributeName, Object> lEntry : aInGeneralAttributeMap
                    .entrySet())
            {
                if (isNullOrEmpty(lEntry.getValue()))
                {
                    continue;
                }
                if (lEntry.getKey() == aInChargingRuleAttributeName)
                {
                    handleTriggerLifeCycleEvent(lEntry.getValue(),
                            aInEntityIdModel);
                }
                else
                {
                    TPAResources.debug(logger,
                            "no result action to process ",
                            "founded in SubscriberPostProvision");
                }
            }
        }
        TPAResources.debug(logger,
                "Exit from processGeneralResultAction method");
    }

    /**
     * handle Trigger LifeCycle Event
     *
     * @param aInValue         List of GenericValueWrapper
     * @param aInEntityIdModel EntityIdModel reference
     */
    private static void handleTriggerLifeCycleEvent(Object aInValue,
            EntityIdModel aInEntityIdModel)
    {
        if (isNotNullOrEmpty(aInValue) && aInValue instanceof List)
        {
            List<GenericValueWrapper> lLifeCycleTriggerEvents =
                    (List<GenericValueWrapper>) aInValue;

            for (GenericValueWrapper lWrapper : lLifeCycleTriggerEvents)
            {
                String lTriggerEventName = lWrapper.getName();
                if (StringUtils.isBlank(lTriggerEventName))
                {
                    TPAResources.debug(logger,
                            SKIP_TRIGGER_EVENT,
                            IS_NULL_EMPTY);
                    continue;
                }

                TriggerLifeCycleEntityType lEntityType =
                        (TriggerLifeCycleEntityType) getConverter()
                                .extractFromGeneric(lWrapper.getValue());
                triggerLifeCycleEvent(lTriggerEventName,
                        aInEntityIdModel, lEntityType);
            }
        }
    }

    /**
     * @param aInGyMessageContext GyMessageContext of SourceContext
     * @return String SubscriptionId
     */
    private static String getSubscriptionId(
            GyMessageContext aInGyMessageContext)
    {
        return null != aInGyMessageContext ?
                aInGyMessageContext.getSubscriptionIdData() : null;

    }

    /**
     * @param aInDeviceContext DeviceContext of SourceContext
     * @return String DeviceId
     */
    private static String getDeviceId(
            DeviceContext aInDeviceContext)
    {
        return isNotNullOrEmpty(aInDeviceContext) ?
                aInDeviceContext.getId() : null;

    }

    /**
     * @param aInAccountContext of SourceContext
     * @return String AccountId
     */
    private static String getAccountId(
            AccountContext aInAccountContext)
    {
        return isNotNullOrEmpty(aInAccountContext) ?
                aInAccountContext.getId() : null;

    }

    /**
     * @param aInUserContext of SourceContext
     * @return String UserId
     */
    private static String getUserId(
            UserContext aInUserContext)
    {
        return isNotNullOrEmpty(aInUserContext) ?
                aInUserContext.getId() : null;

    }

    /**
     * @param aInGroupContext of SourceContext
     * @return String GroupId
     */
    private static String getGroupId(
            GroupContext aInGroupContext)
    {
        return isNotNullOrEmpty(aInGroupContext) ?
                aInGroupContext.getId() : null;

    }

    /**
     * Method to populate Secondary balance
     * @param aInSubscriptionChargingServiceDS
     * @param aInAccountSecondaryBalances
     */
    public static void populateSecondaryBalanceFromSubscriptionChargingServiceDS(List<SubscriptionChargingServiceDS> aInSubscriptionChargingServiceDS,
                                                                                  List<AccountSecondaryBalance> aInAccountSecondaryBalances)
    {
        List<BucketInfo> lBucketInfos = new ArrayList<>();

        for(SubscriptionChargingServiceDS lSubscriptionChargingServiceDS: aInSubscriptionChargingServiceDS)
        {
            lBucketInfos.addAll(lSubscriptionChargingServiceDS.getChargingServiceInst().getBucketInfoList());
        }

        BucketInstanceService lBucketInstanceService = Services.lookup( BucketInstanceService.class);

        for(BucketInfo lBucketInfo: lBucketInfos)
        {
            BucketInstance lBucketInstance = lBucketInstanceService.readBucketInstance(lBucketInfo.getBktInstId());
            if (lBucketInstance != null)
            {
                AccountSecondaryBalance lAccountSecondaryBalance = getAccountSecondaryBalance(lBucketInstance);
                aInAccountSecondaryBalances.add(lAccountSecondaryBalance);
            }
        }
    }

    private static AccountSecondaryBalance getAccountSecondaryBalance(BucketInstance aInBucketInstance)
    {
        AccountSecondaryBalance aOutAccountSecondaryBalance = new AccountSecondaryBalance();
        aOutAccountSecondaryBalance.setValue( aInBucketInstance.getCurrentValue().doubleValue());
        aOutAccountSecondaryBalance.setInitialValue( aInBucketInstance.getInitialValue().doubleValue());
        aOutAccountSecondaryBalance.setBucketName(aInBucketInstance.getBucketDefId());
        if (aInBucketInstance.getEndTime() != null)
        {
            aOutAccountSecondaryBalance.setEndDate(aInBucketInstance.getEndTime());
        }
        return aOutAccountSecondaryBalance;
    }

    /**
     * checks if Wholesale Charging Logic exists
     * on session manager
     * on Terminate request
     * on IMS call
     * and set it on CommonCallRequest of CommonCallContext
     * @param aInRatingEngineRequest RatingEngineRequest
     */
    public static void checkWholesaleCLOnTerminateIMS(
            RatingEngineRequest aInRatingEngineRequest)
    {
        RequestType lRequestType = aInRatingEngineRequest
                .getCall().getCommonCallContext().getRequestType();
        CallType lCallType = aInRatingEngineRequest
                .getCall().getCommonCallContext().getCallType();
        boolean lIsTerminateIms =
                ((RequestType.TERMINATE_REQUEST).equals(lRequestType)
                        && (CallType.IMS).equals(lCallType));
        if (!lIsTerminateIms)
        {
            return;
        }
        WholesaleInstance lWholesaleInstance =
                aInRatingEngineRequest
                        .getCall().getCommonCallContext()
                        .getWholesaleInstanceFromSessionManager();
        boolean lIsValidWholesaleCL =
                (CommonUtil.isNotNullOrEmpty(lWholesaleInstance)
                        && !Strings.isBlank(lWholesaleInstance.getWholesaleCS())
                        && !Strings.isBlank(lWholesaleInstance.getWholesaleRate()));
        if (lIsValidWholesaleCL)
        {
            aInRatingEngineRequest.setWholesaleCLExists(true);
            aInRatingEngineRequest
                    .getCall().getCommonCallContext()
                    .setWholesaleSimulationCS(
                            lWholesaleInstance.getWholesaleCS()
                    );
            aInRatingEngineRequest
                    .getCall().getCommonCallContext()
                    .setWholesaleSimulationRate(
                            lWholesaleInstance.getWholesaleRate()
                    );
        }
    }

    /**
     * Wrapper method to set applicability condition in cache
     *
     * @param aInReRequest                   Rating request
     * @param aInRuleTableForCLApplicability rule table
     * @param aInValue                       the value
     */
    private static void setCLApplicabilityConditionInCache(RatingEngineRequest aInReRequest,
            String aInRuleTableForCLApplicability, boolean aInValue)
    {
        if (aInReRequest.getCall().getGyMessageContext() != null)
        {
            aInReRequest.getCall().getGyMessageContext()
                    .addRuleEvaluationMapEntry(aInRuleTableForCLApplicability, aInValue);
            TPAResources.debug(logger, "Added in cache key :",
                    aInRuleTableForCLApplicability, "  and value :", aInValue);
        }
    }

    /**
     * This method processes intermediate thresholds only for buckets and counters. It marks all
     * the intermediate thresholds crossed. It also fetches latest SNR state among intermediate
     * thresholds if SNR state is not already obtained in latest threshold executed by rule
     * engine.
     *
     * @param aInThrshldPrfContext    Threshold Profile Context
     * @param aInThresholdInstanceMap Threshold instance map containing threshold name as key and
     *                                instance as value
     * @param aInThresholdProfile     Threshold profile object
     * @param aInThrResult            rule result output
     * @param aInPolicyCtrStateFromRule   SNR state from latest threshold executed by rule engine
     * @return
     */
    @Nullable
    private static String processIntermediateThresholdsAndGetSignalingState(
            ThresholdProfileContextImpl aInThrshldPrfContext,
            Map<String, ThresholdInstance> aInThresholdInstanceMap,
            ThresholdProfile aInThresholdProfile, ChargingPolicyDecisionResult aInThrResult,
            String aInPolicyCtrStateFromRule)
    {
        String aOutFinalPolicyCtrState = aInPolicyCtrStateFromRule;
        if (aInThrshldPrfContext.getThresholdEntityType() == ThresholdEntityType.COUNTER ||
                aInThrshldPrfContext.getThresholdEntityType() == ThresholdEntityType.BUCKET)
        {
            String lSignalingStateFromIntermediateThresholds =
                    ThresholdHandler.processIntermediateThresholdsAndGetSignalingState(
                            aInThresholdInstanceMap, aInThresholdProfile, aInThrResult, true);
            // if snr state is not found in latest threshold, get it from intermediate thresholds
            if (aInPolicyCtrStateFromRule == null)
            {
                aOutFinalPolicyCtrState = lSignalingStateFromIntermediateThresholds;
            }
        }
        return aOutFinalPolicyCtrState;
    }
}
