// File: crates/scheduler/Cargo.toml

[package]
name = "scheduler"
version = "0.1.0"
edition.workspace = true
rust-version.workspace = true
license.workspace = true

[features]
default = ["kv_store", "olap"]
olap = ["storage_impl/olap", "hyperswitch_domain_models/olap"]
kv_store = []
email = ["external_services/email"]
v1 = ["diesel_models/v1", "hyperswitch_domain_models/v1", "storage_impl/v1", "common_utils/v1", "common_types/v1"]
v2 = ["diesel_models/v2", "hyperswitch_domain_models/v2", "storage_impl/v2", "common_utils/v2", "common_types/v2"]

[dependencies]
# Third party crates
async-trait = "0.1.88"
error-stack = "0.4.1"
futures = "0.3.31"
num_cpus = "1.16.0"
rand = "0.8.5"
serde = "1.0.219"
serde_json = "1.0.140"
strum = { version = "0.26.3", features = ["derive"] }
thiserror = "1.0.69"
time = { version = "0.3.41", features = ["serde", "serde-well-known", "std"] }
tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread"] }
uuid = { version = "1.17.0", features = ["v4"] }

# First party crates
common_utils = { version = "0.1.0", path = "../common_utils", features = ["signals", "async_ext"] }
common_types = { version = "0.1.0", path = "../common_types" }
diesel_models = { version = "0.1.0", path = "../diesel_models", features = ["kv_store"], default-features = false }
external_services = { version = "0.1.0", path = "../external_services" }
hyperswitch_domain_models = { version = "0.1.0", path = "../hyperswitch_domain_models", default-features = false }
redis_interface = { version = "0.1.0", path = "../redis_interface" }
router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"] }
storage_impl = { version = "0.1.0", path = "../storage_impl", default-features = false }

[lints]
workspace = true


// File: crates/scheduler/src/flow.rs

#[derive(Copy, Clone, Debug, strum::Display, strum::EnumString)]
#[strum(serialize_all = "snake_case")]
pub enum SchedulerFlow {
    Producer,
    Consumer,
    Cleaner,
}


// File: crates/scheduler/src/producer.rs

use std::sync::Arc;

use common_utils::{errors::CustomResult, id_type};
use diesel_models::enums::ProcessTrackerStatus;
use error_stack::{report, ResultExt};
use router_env::{
    instrument,
    tracing::{self, Instrument},
};
use time::Duration;
use tokio::sync::mpsc;

use super::{
    env::logger::{self, debug, error, warn},
    metrics,
};
use crate::{
    configs::settings::SchedulerSettings, errors, flow::SchedulerFlow,
    scheduler::SchedulerInterface, utils::*, SchedulerAppState, SchedulerSessionState,
};

#[instrument(skip_all)]
pub async fn start_producer<T, U, F>(
    state: &T,
    scheduler_settings: Arc<SchedulerSettings>,
    (tx, mut rx): (mpsc::Sender<()>, mpsc::Receiver<()>),
    app_state_to_session_state: F,
) -> CustomResult<(), errors::ProcessTrackerError>
where
    F: Fn(&T, &id_type::TenantId) -> CustomResult<U, errors::ProcessTrackerError>,
    T: SchedulerAppState,
    U: SchedulerSessionState,
{
    use std::time::Duration;

    use rand::distributions::{Distribution, Uniform};

    let mut rng = rand::thread_rng();

    // TODO: this can be removed once rand-0.9 is released
    // reference - https://github.com/rust-random/rand/issues/1326#issuecomment-1635331942
    #[allow(clippy::unnecessary_fallible_conversions)]
    let timeout = Uniform::try_from(0..=scheduler_settings.loop_interval)
        .change_context(errors::ProcessTrackerError::ConfigurationError)?;

    tokio::time::sleep(Duration::from_millis(timeout.sample(&mut rng))).await;

    let mut interval =
        tokio::time::interval(Duration::from_millis(scheduler_settings.loop_interval));

    let mut shutdown_interval = tokio::time::interval(Duration::from_millis(
        scheduler_settings.graceful_shutdown_interval,
    ));

    let signal = common_utils::signals::get_allowed_signals()
        .map_err(|error| {
            logger::error!("Signal Handler Error: {:?}", error);
            errors::ProcessTrackerError::ConfigurationError
        })
        .attach_printable("Failed while creating a signals handler")?;
    let handle = signal.handle();
    let task_handle =
        tokio::spawn(common_utils::signals::signal_handler(signal, tx).in_current_span());

    loop {
        match rx.try_recv() {
            Err(mpsc::error::TryRecvError::Empty) => {
                interval.tick().await;
                let tenants = state.get_tenants();
                for tenant in tenants {
                    let session_state = app_state_to_session_state(state, &tenant)?;
                    match run_producer_flow(&session_state, &scheduler_settings).await {
                        Ok(_) => (),
                        Err(error) => {
                            // Intentionally not propagating error to caller.
                            // Any errors that occur in the producer flow must be handled here only, as
                            // this is the topmost level function which is concerned with the producer flow.
                            error!(?error);
                        }
                    }
                }
            }
            Ok(()) | Err(mpsc::error::TryRecvError::Disconnected) => {
                logger::debug!("Awaiting shutdown!");
                rx.close();
                shutdown_interval.tick().await;
                logger::info!("Terminating producer");
                break;
            }
        }
    }
    handle.close();
    task_handle
        .await
        .change_context(errors::ProcessTrackerError::UnexpectedFlow)?;

    Ok(())
}

#[instrument(skip_all)]
pub async fn run_producer_flow<T>(
    state: &T,
    settings: &SchedulerSettings,
) -> CustomResult<(), errors::ProcessTrackerError>
where
    T: SchedulerSessionState,
{
    lock_acquire_release::<_, _, _>(state.get_db().as_scheduler(), settings, move || async {
        let tasks = fetch_producer_tasks(state.get_db().as_scheduler(), settings).await?;
        debug!("Producer count of tasks {}", tasks.len());

        // [#268]: Allow task based segregation of tasks

        divide_and_append_tasks(
            state.get_db().as_scheduler(),
            SchedulerFlow::Producer,
            tasks,
            settings,
        )
        .await?;

        Ok(())
    })
    .await?;

    Ok(())
}

#[instrument(skip_all)]
pub async fn fetch_producer_tasks(
    db: &dyn SchedulerInterface,
    conf: &SchedulerSettings,
) -> CustomResult<Vec<storage::ProcessTracker>, errors::ProcessTrackerError> {
    let upper = conf.producer.upper_fetch_limit;
    let lower = conf.producer.lower_fetch_limit;
    let now = common_utils::date_time::now();
    // Add these to validations
    let time_upper_limit = now.checked_add(Duration::seconds(upper)).ok_or_else(|| {
        report!(errors::ProcessTrackerError::ConfigurationError)
            .attach_printable("Error obtaining upper limit to fetch producer tasks")
    })?;
    let time_lower_limit = now.checked_sub(Duration::seconds(lower)).ok_or_else(|| {
        report!(errors::ProcessTrackerError::ConfigurationError)
            .attach_printable("Error obtaining lower limit to fetch producer tasks")
    })?;

    let mut new_tasks = db
        .find_processes_by_time_status(
            time_lower_limit,
            time_upper_limit,
            ProcessTrackerStatus::New,
            None,
        )
        .await
        .change_context(errors::ProcessTrackerError::ProcessFetchingFailed)?;
    let mut pending_tasks = db
        .find_processes_by_time_status(
            time_lower_limit,
            time_upper_limit,
            ProcessTrackerStatus::Pending,
            None,
        )
        .await
        .change_context(errors::ProcessTrackerError::ProcessFetchingFailed)?;

    if new_tasks.is_empty() {
        warn!("No new tasks found for producer to schedule");
    }
    if pending_tasks.is_empty() {
        warn!("No pending tasks found for producer to schedule");
    }

    new_tasks.append(&mut pending_tasks);

    // Safety: Assuming we won't deal with more than `u64::MAX` tasks at once
    #[allow(clippy::as_conversions)]
    metrics::TASKS_PICKED_COUNT.add(new_tasks.len() as u64, &[]);
    Ok(new_tasks)
}


// File: crates/scheduler/src/env.rs

#[doc(inline)]
pub use router_env::*;


// File: crates/scheduler/src/configs.rs

pub mod defaults;
pub mod settings;
pub mod validations;


// File: crates/scheduler/src/lib.rs

pub mod configs;
pub mod consumer;
pub mod db;
pub mod env;
pub mod errors;
pub mod flow;
pub mod metrics;
pub mod producer;
pub mod scheduler;
pub mod settings;
pub mod utils;

pub use self::{consumer::types, flow::*, scheduler::*};


// File: crates/scheduler/src/db.rs

pub mod process_tracker;
pub mod queue;


// File: crates/scheduler/src/metrics.rs

use router_env::{counter_metric, global_meter, histogram_metric_f64};

global_meter!(PT_METER, "PROCESS_TRACKER");

histogram_metric_f64!(CONSUMER_OPS, PT_METER);

counter_metric!(PAYMENT_COUNT, PT_METER); // No. of payments created
counter_metric!(TASKS_PICKED_COUNT, PT_METER); // Tasks picked by
counter_metric!(BATCHES_CREATED, PT_METER); // Batches added to stream
counter_metric!(BATCHES_CONSUMED, PT_METER); // Batches consumed by consumer
counter_metric!(TASK_CONSUMED, PT_METER); // Tasks consumed by consumer
counter_metric!(TASK_PROCESSED, PT_METER); // Tasks completed processing
counter_metric!(TASK_FINISHED, PT_METER); // Tasks finished
counter_metric!(TASK_RETRIED, PT_METER); // Tasks added for retries


// File: crates/scheduler/src/scheduler.rs

use std::sync::Arc;

use common_utils::{errors::CustomResult, id_type};
#[cfg(feature = "kv_store")]
use storage_impl::kv_router_store::KVRouterStore;
use storage_impl::mock_db::MockDb;
#[cfg(not(feature = "kv_store"))]
use storage_impl::RouterStore;
use tokio::sync::mpsc;

use super::env::logger::error;
pub use crate::{
    configs::settings::SchedulerSettings,
    consumer::{self, workflows},
    db::{process_tracker::ProcessTrackerInterface, queue::QueueInterface},
    errors,
    flow::SchedulerFlow,
    producer,
};

#[cfg(not(feature = "olap"))]
type StoreType = storage_impl::database::store::Store;
#[cfg(feature = "olap")]
type StoreType = storage_impl::database::store::ReplicaStore;

#[cfg(not(feature = "kv_store"))]
pub type Store = RouterStore<StoreType>;
#[cfg(feature = "kv_store")]
pub type Store = KVRouterStore<StoreType>;

pub trait AsSchedulerInterface {
    fn as_scheduler(&self) -> &dyn SchedulerInterface;
}

impl<T: SchedulerInterface> AsSchedulerInterface for T {
    fn as_scheduler(&self) -> &dyn SchedulerInterface {
        self
    }
}

#[async_trait::async_trait]
pub trait SchedulerInterface:
    ProcessTrackerInterface + QueueInterface + AsSchedulerInterface
{
}

#[async_trait::async_trait]
impl SchedulerInterface for Store {}

#[async_trait::async_trait]
impl SchedulerInterface for MockDb {}

#[async_trait::async_trait]
pub trait SchedulerAppState: Send + Sync + Clone {
    fn get_tenants(&self) -> Vec<id_type::TenantId>;
}
#[async_trait::async_trait]
pub trait SchedulerSessionState: Send + Sync + Clone {
    fn get_db(&self) -> Box<dyn SchedulerInterface>;
}
pub async fn start_process_tracker<
    T: SchedulerAppState + 'static,
    U: SchedulerSessionState + 'static,
    F,
>(
    state: &T,
    scheduler_flow: SchedulerFlow,
    scheduler_settings: Arc<SchedulerSettings>,
    channel: (mpsc::Sender<()>, mpsc::Receiver<()>),
    runner_from_task: impl workflows::ProcessTrackerWorkflows<U> + 'static + Copy + std::fmt::Debug,
    app_state_to_session_state: F,
) -> CustomResult<(), errors::ProcessTrackerError>
where
    F: Fn(&T, &id_type::TenantId) -> CustomResult<U, errors::ProcessTrackerError>,
{
    match scheduler_flow {
        SchedulerFlow::Producer => {
            producer::start_producer(
                state,
                scheduler_settings,
                channel,
                app_state_to_session_state,
            )
            .await?
        }
        SchedulerFlow::Consumer => {
            consumer::start_consumer(
                state,
                scheduler_settings,
                runner_from_task,
                channel,
                app_state_to_session_state,
            )
            .await?
        }
        SchedulerFlow::Cleaner => {
            error!("This flow has not been implemented yet!");
        }
    }
    Ok(())
}


// File: crates/scheduler/src/errors.rs

pub use common_utils::errors::{ParsingError, ValidationError};
#[cfg(feature = "email")]
use external_services::email::EmailError;
use hyperswitch_domain_models::errors::api_error_response::ApiErrorResponse;
pub use redis_interface::errors::RedisError;
pub use storage_impl::errors::ApplicationError;
use storage_impl::errors::{RecoveryError, StorageError};

use crate::env::logger::{self, error};

#[derive(Debug, thiserror::Error)]
pub enum ProcessTrackerError {
    #[error("An unexpected flow was specified")]
    UnexpectedFlow,
    #[error("Failed to serialize object")]
    SerializationFailed,
    #[error("Failed to deserialize object")]
    DeserializationFailed,
    #[error("Missing required field")]
    MissingRequiredField,
    #[error("Failed to insert process batch into stream")]
    BatchInsertionFailed,
    #[error("Failed to insert process into stream")]
    ProcessInsertionFailed,
    #[error("The process batch with the specified details was not found")]
    BatchNotFound,
    #[error("Failed to update process batch in stream")]
    BatchUpdateFailed,
    #[error("Failed to delete process batch from stream")]
    BatchDeleteFailed,
    #[error("An error occurred when trying to read process tracker configuration")]
    ConfigurationError,
    #[error("Failed to update process in database")]
    ProcessUpdateFailed,
    #[error("Failed to fetch processes from database")]
    ProcessFetchingFailed,
    #[error("Failed while fetching: {resource_name}")]
    ResourceFetchingFailed { resource_name: String },
    #[error("Failed while executing: {flow}")]
    FlowExecutionError { flow: &'static str },
    #[error("Not Implemented")]
    NotImplemented,
    #[error("Job not found")]
    JobNotFound,
    #[error("Received Error ApiResponseError")]
    EApiErrorResponse,
    #[error("Received Error ClientError")]
    EClientError,
    #[error("Received RecoveryError: {0:?}")]
    ERecoveryError(error_stack::Report<RecoveryError>),
    #[error("Received Error StorageError: {0:?}")]
    EStorageError(error_stack::Report<StorageError>),
    #[error("Received Error RedisError: {0:?}")]
    ERedisError(error_stack::Report<RedisError>),
    #[error("Received Error ParsingError: {0:?}")]
    EParsingError(error_stack::Report<ParsingError>),
    #[error("Validation Error Received: {0:?}")]
    EValidationError(error_stack::Report<ValidationError>),
    #[cfg(feature = "email")]
    #[error("Received Error EmailError: {0:?}")]
    EEmailError(error_stack::Report<EmailError>),
    #[error("Type Conversion error")]
    TypeConversionError,
    #[error("Tenant not found")]
    TenantNotFound,
}

#[macro_export]
macro_rules! error_to_process_tracker_error {
    ($($path: ident)::+ < $st: ident >, $($path2:ident)::* ($($inner_path2:ident)::+ <$st2:ident>) ) => {
        impl From<$($path)::+ <$st>> for ProcessTrackerError {
            fn from(err: $($path)::+ <$st> ) -> Self {
                $($path2)::*(err)
            }
        }
    };

    ($($path: ident)::+  <$($inner_path:ident)::+>, $($path2:ident)::* ($($inner_path2:ident)::+ <$st2:ident>) ) => {
        impl<'a> From< $($path)::+ <$($inner_path)::+> > for ProcessTrackerError {
            fn from(err: $($path)::+ <$($inner_path)::+> ) -> Self {
                $($path2)::*(err)
            }
        }
    };
}
pub trait PTError: Send + Sync + 'static {
    fn to_pt_error(&self) -> ProcessTrackerError;
}

impl<T: PTError> From<T> for ProcessTrackerError {
    fn from(value: T) -> Self {
        value.to_pt_error()
    }
}

impl PTError for ApiErrorResponse {
    fn to_pt_error(&self) -> ProcessTrackerError {
        ProcessTrackerError::EApiErrorResponse
    }
}

impl<T: PTError + std::fmt::Debug + std::fmt::Display> From<error_stack::Report<T>>
    for ProcessTrackerError
{
    fn from(error: error_stack::Report<T>) -> Self {
        logger::error!(?error);
        error.current_context().to_pt_error()
    }
}

error_to_process_tracker_error!(
    error_stack::Report<StorageError>,
    ProcessTrackerError::EStorageError(error_stack::Report<StorageError>)
);

error_to_process_tracker_error!(
    error_stack::Report<RedisError>,
    ProcessTrackerError::ERedisError(error_stack::Report<RedisError>)
);

error_to_process_tracker_error!(
    error_stack::Report<ParsingError>,
    ProcessTrackerError::EParsingError(error_stack::Report<ParsingError>)
);

error_to_process_tracker_error!(
    error_stack::Report<ValidationError>,
    ProcessTrackerError::EValidationError(error_stack::Report<ValidationError>)
);

#[cfg(feature = "email")]
error_to_process_tracker_error!(
    error_stack::Report<EmailError>,
    ProcessTrackerError::EEmailError(error_stack::Report<EmailError>)
);

error_to_process_tracker_error!(
    error_stack::Report<RecoveryError>,
    ProcessTrackerError::ERecoveryError(error_stack::Report<RecoveryError>)
);


// File: crates/scheduler/src/settings.rs

use common_utils::ext_traits::ConfigExt;
use serde::Deserialize;
use storage_impl::errors::ApplicationError;

pub use crate::configs::settings::SchedulerSettings;

#[derive(Debug, Clone, Deserialize)]
pub struct ProducerSettings {
    pub upper_fetch_limit: i64,
    pub lower_fetch_limit: i64,

    pub lock_key: String,
    pub lock_ttl: i64,
    pub batch_size: usize,
}

#[derive(Debug, Clone, Deserialize)]
pub struct ConsumerSettings {
    pub disabled: bool,
    pub consumer_group: String,
}

#[cfg(feature = "kv_store")]
#[derive(Debug, Clone, Deserialize)]
pub struct DrainerSettings {
    pub stream_name: String,
    pub num_partitions: u8,
    pub max_read_count: u64,
    pub shutdown_interval: u32, // in milliseconds
    pub loop_interval: u32,     // in milliseconds
}

impl ProducerSettings {
    pub fn validate(&self) -> Result<(), ApplicationError> {
        common_utils::fp_utils::when(self.lock_key.is_default_or_empty(), || {
            Err(ApplicationError::InvalidConfigurationValueError(
                "producer lock key must not be empty".into(),
            ))
        })
    }
}

#[cfg(feature = "kv_store")]
impl DrainerSettings {
    pub fn validate(&self) -> Result<(), ApplicationError> {
        common_utils::fp_utils::when(self.stream_name.is_default_or_empty(), || {
            Err(ApplicationError::InvalidConfigurationValueError(
                "drainer stream name must not be empty".into(),
            ))
        })
    }
}

impl Default for ProducerSettings {
    fn default() -> Self {
        Self {
            upper_fetch_limit: 0,
            lower_fetch_limit: 1800,
            lock_key: "PRODUCER_LOCKING_KEY".into(),
            lock_ttl: 160,
            batch_size: 200,
        }
    }
}

impl Default for ConsumerSettings {
    fn default() -> Self {
        Self {
            disabled: false,
            consumer_group: "SCHEDULER_GROUP".into(),
        }
    }
}

#[cfg(feature = "kv_store")]
impl Default for DrainerSettings {
    fn default() -> Self {
        Self {
            stream_name: "DRAINER_STREAM".into(),
            num_partitions: 64,
            max_read_count: 100,
            shutdown_interval: 1000,
            loop_interval: 500,
        }
    }
}


// File: crates/scheduler/src/consumer.rs

// TODO: Figure out what to log

use std::{
    sync::{self, atomic},
    time as std_time,
};
pub mod types;
pub mod workflows;

use common_utils::{errors::CustomResult, id_type, signals::get_allowed_signals};
use diesel_models::enums;
pub use diesel_models::{self, process_tracker as storage};
use error_stack::ResultExt;
use futures::future;
use redis_interface::{RedisConnectionPool, RedisEntryId};
use router_env::{
    instrument,
    tracing::{self, Instrument},
};
use time::PrimitiveDateTime;
use tokio::sync::mpsc;
use uuid::Uuid;

use super::env::logger;
pub use super::workflows::ProcessTrackerWorkflow;
use crate::{
    configs::settings::SchedulerSettings, db::process_tracker::ProcessTrackerInterface, errors,
    metrics, utils as pt_utils, SchedulerAppState, SchedulerInterface, SchedulerSessionState,
};

// Valid consumer business statuses
pub fn valid_business_statuses() -> Vec<&'static str> {
    vec![storage::business_status::PENDING]
}

#[instrument(skip_all)]
pub async fn start_consumer<T: SchedulerAppState + 'static, U: SchedulerSessionState + 'static, F>(
    state: &T,
    settings: sync::Arc<SchedulerSettings>,
    workflow_selector: impl workflows::ProcessTrackerWorkflows<U> + 'static + Copy + std::fmt::Debug,
    (tx, mut rx): (mpsc::Sender<()>, mpsc::Receiver<()>),
    app_state_to_session_state: F,
) -> CustomResult<(), errors::ProcessTrackerError>
where
    F: Fn(&T, &id_type::TenantId) -> CustomResult<U, errors::ProcessTrackerError>,
{
    use std::time::Duration;

    use rand::distributions::{Distribution, Uniform};

    let mut rng = rand::thread_rng();

    // TODO: this can be removed once rand-0.9 is released
    // reference - https://github.com/rust-random/rand/issues/1326#issuecomment-1635331942
    #[allow(clippy::unnecessary_fallible_conversions)]
    let timeout = Uniform::try_from(0..=settings.loop_interval)
        .change_context(errors::ProcessTrackerError::ConfigurationError)?;

    tokio::time::sleep(Duration::from_millis(timeout.sample(&mut rng))).await;

    let mut interval = tokio::time::interval(Duration::from_millis(settings.loop_interval));

    let mut shutdown_interval =
        tokio::time::interval(Duration::from_millis(settings.graceful_shutdown_interval));

    let consumer_operation_counter = sync::Arc::new(atomic::AtomicU64::new(0));
    let signal = get_allowed_signals()
        .map_err(|error| {
            logger::error!(?error, "Signal Handler Error");
            errors::ProcessTrackerError::ConfigurationError
        })
        .attach_printable("Failed while creating a signals handler")?;
    let handle = signal.handle();
    let task_handle =
        tokio::spawn(common_utils::signals::signal_handler(signal, tx).in_current_span());

    'consumer: loop {
        match rx.try_recv() {
            Err(mpsc::error::TryRecvError::Empty) => {
                interval.tick().await;

                // A guard from env to disable the consumer
                if settings.consumer.disabled {
                    continue;
                }
                consumer_operation_counter.fetch_add(1, atomic::Ordering::SeqCst);
                let start_time = std_time::Instant::now();
                let tenants = state.get_tenants();
                for tenant in tenants {
                    let session_state = app_state_to_session_state(state, &tenant)?;
                    pt_utils::consumer_operation_handler(
                        session_state.clone(),
                        settings.clone(),
                        |error| {
                            logger::error!(?error, "Failed to perform consumer operation");
                        },
                        workflow_selector,
                    )
                    .await;
                }

                let end_time = std_time::Instant::now();
                let duration = end_time.saturating_duration_since(start_time).as_secs_f64();
                logger::debug!("Time taken to execute consumer_operation: {}s", duration);

                let current_count =
                    consumer_operation_counter.fetch_sub(1, atomic::Ordering::SeqCst);
                logger::info!("Current tasks being executed: {}", current_count);
            }
            Ok(()) | Err(mpsc::error::TryRecvError::Disconnected) => {
                logger::debug!("Awaiting shutdown!");
                rx.close();
                loop {
                    shutdown_interval.tick().await;
                    let active_tasks = consumer_operation_counter.load(atomic::Ordering::Acquire);
                    logger::info!("Active tasks: {active_tasks}");
                    match active_tasks {
                        0 => {
                            logger::info!("Terminating consumer");
                            break 'consumer;
                        }
                        _ => continue,
                    }
                }
            }
        }
    }
    handle.close();
    task_handle
        .await
        .change_context(errors::ProcessTrackerError::UnexpectedFlow)?;

    Ok(())
}

#[instrument(skip_all)]
pub async fn consumer_operations<T: SchedulerSessionState + 'static>(
    state: &T,
    settings: &SchedulerSettings,
    workflow_selector: impl workflows::ProcessTrackerWorkflows<T> + 'static + Copy + std::fmt::Debug,
) -> CustomResult<(), errors::ProcessTrackerError> {
    let stream_name = settings.stream.clone();
    let group_name = settings.consumer.consumer_group.clone();
    let consumer_name = format!("consumer_{}", Uuid::new_v4());

    let _group_created = &mut state
        .get_db()
        .consumer_group_create(&stream_name, &group_name, &RedisEntryId::AfterLastID)
        .await;

    let mut tasks = state
        .get_db()
        .as_scheduler()
        .fetch_consumer_tasks(&stream_name, &group_name, &consumer_name)
        .await?;

    if !tasks.is_empty() {
        logger::info!("{} picked {} tasks", consumer_name, tasks.len());
    }
    let mut handler = vec![];

    for task in tasks.iter_mut() {
        let pickup_time = common_utils::date_time::now();

        pt_utils::add_histogram_metrics(&pickup_time, task, &stream_name);

        metrics::TASK_CONSUMED.add(1, &[]);

        handler.push(tokio::task::spawn(start_workflow(
            state.clone(),
            task.clone(),
            pickup_time,
            workflow_selector,
        )))
    }
    future::join_all(handler).await;

    Ok(())
}

#[instrument(skip(db, redis_conn))]
pub async fn fetch_consumer_tasks(
    db: &dyn ProcessTrackerInterface,
    redis_conn: &RedisConnectionPool,
    stream_name: &str,
    group_name: &str,
    consumer_name: &str,
) -> CustomResult<Vec<storage::ProcessTracker>, errors::ProcessTrackerError> {
    let batches = pt_utils::get_batches(redis_conn, stream_name, group_name, consumer_name).await?;

    // Returning early to avoid execution of database queries when `batches` is empty
    if batches.is_empty() {
        return Ok(Vec::new());
    }

    let mut tasks = batches.into_iter().fold(Vec::new(), |mut acc, batch| {
        acc.extend_from_slice(
            batch
                .trackers
                .into_iter()
                .filter(|task| task.is_valid_business_status(&valid_business_statuses()))
                .collect::<Vec<_>>()
                .as_slice(),
        );
        acc
    });
    let task_ids = tasks
        .iter()
        .map(|task| task.id.to_owned())
        .collect::<Vec<_>>();

    db.process_tracker_update_process_status_by_ids(
        task_ids,
        storage::ProcessTrackerUpdate::StatusUpdate {
            status: enums::ProcessTrackerStatus::ProcessStarted,
            business_status: None,
        },
    )
    .await
    .change_context(errors::ProcessTrackerError::ProcessFetchingFailed)?;
    tasks
        .iter_mut()
        .for_each(|x| x.status = enums::ProcessTrackerStatus::ProcessStarted);
    Ok(tasks)
}

// Accept flow_options if required
#[instrument(skip(state), fields(workflow_id))]
pub async fn start_workflow<T>(
    state: T,
    process: storage::ProcessTracker,
    _pickup_time: PrimitiveDateTime,
    workflow_selector: impl workflows::ProcessTrackerWorkflows<T> + 'static + std::fmt::Debug,
) -> CustomResult<(), errors::ProcessTrackerError>
where
    T: SchedulerSessionState,
{
    tracing::Span::current().record("workflow_id", Uuid::new_v4().to_string());
    logger::info!(pt.name=?process.name, pt.id=%process.id);
    let res = workflow_selector
        .trigger_workflow(&state.clone(), process.clone())
        .await
        .inspect_err(|error| {
            logger::error!(?error, "Failed to trigger workflow");
        });
    metrics::TASK_PROCESSED.add(1, &[]);
    res
}

#[instrument(skip_all)]
pub async fn consumer_error_handler(
    state: &(dyn SchedulerInterface + 'static),
    process: storage::ProcessTracker,
    error: errors::ProcessTrackerError,
) -> CustomResult<(), errors::ProcessTrackerError> {
    logger::error!(pt.name=?process.name, pt.id=%process.id, ?error, "Failed to execute workflow");

    state
        .process_tracker_update_process_status_by_ids(
            vec![process.id],
            storage::ProcessTrackerUpdate::StatusUpdate {
                status: enums::ProcessTrackerStatus::Finish,
                business_status: Some(String::from(storage::business_status::GLOBAL_ERROR)),
            },
        )
        .await
        .change_context(errors::ProcessTrackerError::ProcessUpdateFailed)?;
    Ok(())
}

pub async fn create_task(
    db: &dyn ProcessTrackerInterface,
    process_tracker_entry: storage::ProcessTrackerNew,
) -> CustomResult<(), storage_impl::errors::StorageError> {
    db.insert_process(process_tracker_entry).await?;
    Ok(())
}


// File: crates/scheduler/src/utils.rs

use std::sync;

use common_utils::errors::CustomResult;
use diesel_models::enums::{self, ProcessTrackerStatus};
pub use diesel_models::process_tracker as storage;
use error_stack::{report, ResultExt};
use redis_interface::{RedisConnectionPool, RedisEntryId};
use router_env::{instrument, tracing};
use uuid::Uuid;

use super::{
    consumer::{self, types::process_data, workflows},
    env::logger,
};
use crate::{
    configs::settings::SchedulerSettings, consumer::types::ProcessTrackerBatch, errors,
    flow::SchedulerFlow, metrics, SchedulerInterface, SchedulerSessionState,
};

pub async fn divide_and_append_tasks<T>(
    state: &T,
    flow: SchedulerFlow,
    tasks: Vec<storage::ProcessTracker>,
    settings: &SchedulerSettings,
) -> CustomResult<(), errors::ProcessTrackerError>
where
    T: SchedulerInterface + Send + Sync + ?Sized,
{
    let batches = divide(tasks, settings);
    // Safety: Assuming we won't deal with more than `u64::MAX` batches at once
    logger::info!("Adding {} batches to stream", batches.len());
    #[allow(clippy::as_conversions)]
    metrics::BATCHES_CREATED.add(batches.len() as u64, &[]); // Metrics
    for batch in batches {
        let result = update_status_and_append(state, flow, batch).await;
        match result {
            Ok(_) => (),
            Err(error) => logger::error!(?error),
        }
    }
    Ok(())
}

pub async fn update_status_and_append<T>(
    state: &T,
    flow: SchedulerFlow,
    pt_batch: ProcessTrackerBatch,
) -> CustomResult<(), errors::ProcessTrackerError>
where
    T: SchedulerInterface + Send + Sync + ?Sized,
{
    let process_ids: Vec<String> = pt_batch
        .trackers
        .iter()
        .map(|process| process.id.to_owned())
        .collect();
    match flow {
        SchedulerFlow::Producer => state
            .process_tracker_update_process_status_by_ids(
                process_ids,
                storage::ProcessTrackerUpdate::StatusUpdate {
                    status: ProcessTrackerStatus::Processing,
                    business_status: None,
                },
            )
            .await
            .map_or_else(
                |error| {
                    logger::error!(?error, "Error while updating process status");
                    Err(error.change_context(errors::ProcessTrackerError::ProcessUpdateFailed))
                },
                |count| {
                    logger::debug!("Updated status of {count} processes");
                    Ok(())
                },
            ),
        SchedulerFlow::Cleaner => {
            let res = state
                .reinitialize_limbo_processes(process_ids, common_utils::date_time::now())
                .await;
            match res {
                Ok(count) => {
                    logger::debug!("Reinitialized {count} processes");
                    Ok(())
                }
                Err(error) => {
                    logger::error!(?error, "Error while reinitializing processes");
                    Err(error.change_context(errors::ProcessTrackerError::ProcessUpdateFailed))
                }
            }
        }
        _ => {
            let error = format!("Unexpected scheduler flow {flow:?}");
            logger::error!(%error);
            Err(report!(errors::ProcessTrackerError::UnexpectedFlow).attach_printable(error))
        }
    }?;

    let field_value_pairs = pt_batch.to_redis_field_value_pairs()?;

    match state
        .stream_append_entry(
            &pt_batch.stream_name,
            &RedisEntryId::AutoGeneratedID,
            field_value_pairs,
        )
        .await
        .change_context(errors::ProcessTrackerError::BatchInsertionFailed)
    {
        Ok(x) => Ok(x),
        Err(mut err) => {
            let update_res = state
                .process_tracker_update_process_status_by_ids(
                    pt_batch
                        .trackers
                        .iter()
                        .map(|process| process.id.clone())
                        .collect(),
                    storage::ProcessTrackerUpdate::StatusUpdate {
                        status: ProcessTrackerStatus::Processing,
                        business_status: None,
                    },
                )
                .await
                .map_or_else(
                    |error| {
                        logger::error!(?error, "Error while updating process status");
                        Err(error.change_context(errors::ProcessTrackerError::ProcessUpdateFailed))
                    },
                    |count| {
                        logger::debug!("Updated status of {count} processes");
                        Ok(())
                    },
                );

            match update_res {
                Ok(_) => (),
                Err(inner_err) => {
                    err.extend_one(inner_err);
                }
            };

            Err(err)
        }
    }
}

pub fn divide(
    tasks: Vec<storage::ProcessTracker>,
    conf: &SchedulerSettings,
) -> Vec<ProcessTrackerBatch> {
    let now = common_utils::date_time::now();
    let batch_size = conf.producer.batch_size;
    divide_into_batches(batch_size, tasks, now, conf)
}

pub fn divide_into_batches(
    batch_size: usize,
    tasks: Vec<storage::ProcessTracker>,
    batch_creation_time: time::PrimitiveDateTime,
    conf: &SchedulerSettings,
) -> Vec<ProcessTrackerBatch> {
    let batch_id = Uuid::new_v4().to_string();

    tasks
        .chunks(batch_size)
        .fold(Vec::new(), |mut batches, item| {
            let batch = ProcessTrackerBatch {
                id: batch_id.clone(),
                group_name: conf.consumer.consumer_group.clone(),
                stream_name: conf.stream.clone(),
                connection_name: String::new(),
                created_time: batch_creation_time,
                rule: String::new(), // is it required?
                trackers: item.to_vec(),
            };
            batches.push(batch);

            batches
        })
}

pub async fn get_batches(
    conn: &RedisConnectionPool,
    stream_name: &str,
    group_name: &str,
    consumer_name: &str,
) -> CustomResult<Vec<ProcessTrackerBatch>, errors::ProcessTrackerError> {
    let response = match conn
        .stream_read_with_options(
            stream_name,
            RedisEntryId::UndeliveredEntryID,
            // Update logic for collecting to Vec and flattening, if count > 1 is provided
            Some(1),
            None,
            Some((group_name, consumer_name)),
        )
        .await
    {
        Ok(response) => response,
        Err(error) => {
            if let redis_interface::errors::RedisError::StreamEmptyOrNotAvailable =
                error.current_context()
            {
                logger::debug!("No batches processed as stream is empty");
                return Ok(Vec::new());
            } else {
                return Err(error.change_context(errors::ProcessTrackerError::BatchNotFound));
            }
        }
    };

    metrics::BATCHES_CONSUMED.add(1, &[]);

    let (batches, entry_ids): (Vec<Vec<ProcessTrackerBatch>>, Vec<Vec<String>>) = response.into_values().map(|entries| {
        entries.into_iter().try_fold(
            (Vec::new(), Vec::new()),
            |(mut batches, mut entry_ids), entry| {
                // Redis entry ID
                entry_ids.push(entry.0);
                // Value HashMap
                batches.push(ProcessTrackerBatch::from_redis_stream_entry(entry.1)?);

                Ok((batches, entry_ids))
            },
        )
    }).collect::<CustomResult<Vec<(Vec<ProcessTrackerBatch>, Vec<String>)>, errors::ProcessTrackerError>>()?
    .into_iter()
    .unzip();
    // Flattening the Vec's since the count provided above is 1. This needs to be updated if a
    // count greater than 1 is provided.
    let batches = batches.into_iter().flatten().collect::<Vec<_>>();
    let entry_ids = entry_ids.into_iter().flatten().collect::<Vec<_>>();

    conn.stream_acknowledge_entries(&stream_name.into(), group_name, entry_ids.clone())
        .await
        .map_err(|error| {
            logger::error!(?error, "Error acknowledging batch in stream");
            error.change_context(errors::ProcessTrackerError::BatchUpdateFailed)
        })?;
    conn.stream_delete_entries(&stream_name.into(), entry_ids.clone())
        .await
        .map_err(|error| {
            logger::error!(?error, "Error deleting batch from stream");
            error.change_context(errors::ProcessTrackerError::BatchDeleteFailed)
        })?;

    Ok(batches)
}

pub fn get_process_tracker_id<'a>(
    runner: storage::ProcessTrackerRunner,
    task_name: &'a str,
    txn_id: &'a str,
    merchant_id: &'a common_utils::id_type::MerchantId,
) -> String {
    format!(
        "{runner}_{task_name}_{txn_id}_{}",
        merchant_id.get_string_repr()
    )
}

pub fn get_process_tracker_id_for_dispute_list<'a>(
    runner: storage::ProcessTrackerRunner,
    merchant_connector_account_id: &'a common_utils::id_type::MerchantConnectorAccountId,
    created_from: time::PrimitiveDateTime,
    merchant_id: &'a common_utils::id_type::MerchantId,
) -> String {
    format!(
        "{runner}_{:04}{}{:02}{:02}_{}_{}",
        created_from.year(),
        created_from.month(),
        created_from.day(),
        created_from.hour(),
        merchant_connector_account_id.get_string_repr(),
        merchant_id.get_string_repr()
    )
}

pub fn get_time_from_delta(delta: Option<i32>) -> Option<time::PrimitiveDateTime> {
    delta.map(|t| common_utils::date_time::now().saturating_add(time::Duration::seconds(t.into())))
}

#[instrument(skip_all)]
pub async fn consumer_operation_handler<E, T>(
    state: T,
    settings: sync::Arc<SchedulerSettings>,
    error_handler_fun: E,
    workflow_selector: impl workflows::ProcessTrackerWorkflows<T> + 'static + Copy + std::fmt::Debug,
) where
    // Error handler function
    E: FnOnce(error_stack::Report<errors::ProcessTrackerError>),
    T: SchedulerSessionState + Send + Sync + 'static,
{
    match consumer::consumer_operations(&state, &settings, workflow_selector).await {
        Ok(_) => (),
        Err(err) => error_handler_fun(err),
    }
}

pub fn add_histogram_metrics(
    pickup_time: &time::PrimitiveDateTime,
    task: &mut storage::ProcessTracker,
    stream_name: &str,
) {
    #[warn(clippy::option_map_unit_fn)]
    if let Some((schedule_time, runner)) = task.schedule_time.as_ref().zip(task.runner.as_ref()) {
        let pickup_schedule_delta = (*pickup_time - *schedule_time).as_seconds_f64();
        logger::info!("Time delta for scheduled tasks: {pickup_schedule_delta} seconds");
        let runner_name = runner.clone();
        metrics::CONSUMER_OPS.record(
            pickup_schedule_delta,
            router_env::metric_attributes!((stream_name.to_owned(), runner_name)),
        );
    };
}

pub fn get_schedule_time(
    mapping: process_data::ConnectorPTMapping,
    merchant_id: &common_utils::id_type::MerchantId,
    retry_count: i32,
) -> Option<i32> {
    let mapping = match mapping.custom_merchant_mapping.get(merchant_id) {
        Some(map) => map.clone(),
        None => mapping.default_mapping,
    };

    // For first try, get the `start_after` time
    if retry_count == 0 {
        Some(mapping.start_after)
    } else {
        get_delay(retry_count, &mapping.frequencies)
    }
}

pub fn get_pm_schedule_time(
    mapping: process_data::PaymentMethodsPTMapping,
    pm: enums::PaymentMethod,
    retry_count: i32,
) -> Option<i32> {
    let mapping = match mapping.custom_pm_mapping.get(&pm) {
        Some(map) => map.clone(),
        None => mapping.default_mapping,
    };

    if retry_count == 0 {
        Some(mapping.start_after)
    } else {
        get_delay(retry_count, &mapping.frequencies)
    }
}

pub fn get_outgoing_webhook_retry_schedule_time(
    mapping: process_data::OutgoingWebhookRetryProcessTrackerMapping,
    merchant_id: &common_utils::id_type::MerchantId,
    retry_count: i32,
) -> Option<i32> {
    let retry_mapping = match mapping.custom_merchant_mapping.get(merchant_id) {
        Some(map) => map.clone(),
        None => mapping.default_mapping,
    };

    // For first try, get the `start_after` time
    if retry_count == 0 {
        Some(retry_mapping.start_after)
    } else {
        get_delay(retry_count, &retry_mapping.frequencies)
    }
}

pub fn get_pcr_payments_retry_schedule_time(
    mapping: process_data::RevenueRecoveryPaymentProcessTrackerMapping,
    merchant_id: &common_utils::id_type::MerchantId,
    retry_count: i32,
) -> Option<i32> {
    let mapping = match mapping.custom_merchant_mapping.get(merchant_id) {
        Some(map) => map.clone(),
        None => mapping.default_mapping,
    };
    // TODO: check if the current scheduled time is not more than the configured timerange

    // For first try, get the `start_after` time
    if retry_count == 0 {
        Some(mapping.start_after)
    } else {
        get_delay(retry_count, &mapping.frequencies)
    }
}

pub fn get_subscription_invoice_sync_retry_schedule_time(
    mapping: process_data::SubscriptionInvoiceSyncPTMapping,
    merchant_id: &common_utils::id_type::MerchantId,
    retry_count: i32,
) -> Option<i32> {
    let mapping = match mapping.custom_merchant_mapping.get(merchant_id) {
        Some(map) => map.clone(),
        None => mapping.default_mapping,
    };

    if retry_count == 0 {
        Some(mapping.start_after)
    } else {
        get_delay(retry_count, &mapping.frequencies)
    }
}

/// Get the delay based on the retry count
pub fn get_delay<'a>(
    retry_count: i32,
    frequencies: impl IntoIterator<Item = &'a (i32, i32)>,
) -> Option<i32> {
    // Preferably, fix this by using unsigned ints
    if retry_count <= 0 {
        return None;
    }

    let mut cumulative_count = 0;
    for &(frequency, count) in frequencies.into_iter() {
        cumulative_count += count;
        if cumulative_count >= retry_count {
            return Some(frequency);
        }
    }

    None
}

pub(crate) async fn lock_acquire_release<T, F, Fut>(
    state: &T,
    settings: &SchedulerSettings,
    callback: F,
) -> CustomResult<(), errors::ProcessTrackerError>
where
    F: Fn() -> Fut,
    T: SchedulerInterface + Send + Sync + ?Sized,
    Fut: futures::Future<Output = CustomResult<(), errors::ProcessTrackerError>>,
{
    let tag = "PRODUCER_LOCK";
    let lock_key = &settings.producer.lock_key;
    let lock_val = "LOCKED";
    let ttl = settings.producer.lock_ttl;

    if state
        .acquire_pt_lock(tag, lock_key, lock_val, ttl)
        .await
        .change_context(errors::ProcessTrackerError::ERedisError(
            errors::RedisError::RedisConnectionError.into(),
        ))?
    {
        let result = callback().await;
        state
            .release_pt_lock(tag, lock_key)
            .await
            .map_err(errors::ProcessTrackerError::ERedisError)?;
        result
    } else {
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_get_delay() {
        let frequency_count = vec![(300, 10), (600, 5), (1800, 3), (3600, 2)];

        let retry_counts_and_expected_delays = [
            (-4, None),
            (-2, None),
            (0, None),
            (4, Some(300)),
            (7, Some(300)),
            (10, Some(300)),
            (12, Some(600)),
            (16, Some(1800)),
            (18, Some(1800)),
            (20, Some(3600)),
            (24, None),
            (30, None),
        ];

        for (retry_count, expected_delay) in retry_counts_and_expected_delays {
            let delay = get_delay(retry_count, &frequency_count);

            assert_eq!(
                delay, expected_delay,
                "Delay and expected delay differ for `retry_count` = {retry_count}"
            );
        }
    }
}


// File: crates/scheduler/src/configs/defaults.rs

impl Default for super::settings::SchedulerSettings {
    fn default() -> Self {
        Self {
            stream: "SCHEDULER_STREAM".into(),
            producer: super::settings::ProducerSettings::default(),
            consumer: super::settings::ConsumerSettings::default(),
            graceful_shutdown_interval: 60000,
            loop_interval: 5000,
            server: super::settings::Server::default(),
        }
    }
}

impl Default for super::settings::ProducerSettings {
    fn default() -> Self {
        Self {
            upper_fetch_limit: 0,
            lower_fetch_limit: 1800,
            lock_key: "PRODUCER_LOCKING_KEY".into(),
            lock_ttl: 160,
            batch_size: 200,
        }
    }
}

impl Default for super::settings::ConsumerSettings {
    fn default() -> Self {
        Self {
            disabled: false,
            consumer_group: "SCHEDULER_GROUP".into(),
        }
    }
}

impl Default for super::settings::Server {
    fn default() -> Self {
        Self {
            port: 8080,
            workers: num_cpus::get_physical(),
            host: "localhost".into(),
        }
    }
}


// File: crates/scheduler/src/configs/settings.rs

pub use router_env::config::{Log, LogConsole, LogFile, LogTelemetry};
use serde::Deserialize;

#[derive(Debug, Clone, Deserialize)]
#[serde(default)]
pub struct SchedulerSettings {
    pub stream: String,
    pub producer: ProducerSettings,
    pub consumer: ConsumerSettings,
    pub loop_interval: u64,
    pub graceful_shutdown_interval: u64,
    pub server: Server,
}

#[derive(Debug, Deserialize, Clone)]
#[serde(default)]
pub struct Server {
    pub port: u16,
    pub workers: usize,
    pub host: String,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(default)]
pub struct ProducerSettings {
    pub upper_fetch_limit: i64,
    pub lower_fetch_limit: i64,

    pub lock_key: String,
    pub lock_ttl: i64,
    pub batch_size: usize,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(default)]
pub struct ConsumerSettings {
    pub disabled: bool,
    pub consumer_group: String,
}


// File: crates/scheduler/src/configs/validations.rs

use common_utils::ext_traits::ConfigExt;
use storage_impl::errors::ApplicationError;

impl super::settings::SchedulerSettings {
    pub fn validate(&self) -> Result<(), ApplicationError> {
        use common_utils::fp_utils::when;

        when(self.stream.is_default_or_empty(), || {
            Err(ApplicationError::InvalidConfigurationValueError(
                "scheduler stream must not be empty".into(),
            ))
        })?;

        when(self.consumer.consumer_group.is_default_or_empty(), || {
            Err(ApplicationError::InvalidConfigurationValueError(
                "scheduler consumer group must not be empty".into(),
            ))
        })?;

        self.producer.validate()?;

        self.server.validate()?;

        Ok(())
    }
}

impl super::settings::ProducerSettings {
    pub fn validate(&self) -> Result<(), ApplicationError> {
        common_utils::fp_utils::when(self.lock_key.is_default_or_empty(), || {
            Err(ApplicationError::InvalidConfigurationValueError(
                "producer lock key must not be empty".into(),
            ))
        })
    }
}

impl super::settings::Server {
    pub fn validate(&self) -> Result<(), ApplicationError> {
        common_utils::fp_utils::when(self.host.is_default_or_empty(), || {
            Err(ApplicationError::InvalidConfigurationValueError(
                "server host must not be empty".into(),
            ))
        })
    }
}


// File: crates/scheduler/src/db/queue.rs

use common_utils::errors::CustomResult;
use diesel_models::process_tracker as storage;
use redis_interface::{errors::RedisError, RedisEntryId, SetnxReply};
use router_env::logger;
use storage_impl::{mock_db::MockDb, redis::kv_store::RedisConnInterface};

use crate::{errors::ProcessTrackerError, scheduler::Store};

#[async_trait::async_trait]
pub trait QueueInterface {
    async fn fetch_consumer_tasks(
        &self,
        stream_name: &str,
        group_name: &str,
        consumer_name: &str,
    ) -> CustomResult<Vec<storage::ProcessTracker>, ProcessTrackerError>;

    async fn consumer_group_create(
        &self,
        stream: &str,
        group: &str,
        id: &RedisEntryId,
    ) -> CustomResult<(), RedisError>;

    async fn acquire_pt_lock(
        &self,
        tag: &str,
        lock_key: &str,
        lock_val: &str,
        ttl: i64,
    ) -> CustomResult<bool, RedisError>;

    async fn release_pt_lock(&self, tag: &str, lock_key: &str) -> CustomResult<bool, RedisError>;

    async fn stream_append_entry(
        &self,
        stream: &str,
        entry_id: &RedisEntryId,
        fields: Vec<(&str, String)>,
    ) -> CustomResult<(), RedisError>;

    async fn get_key(&self, key: &str) -> CustomResult<Vec<u8>, RedisError>;
}

#[async_trait::async_trait]
impl QueueInterface for Store {
    async fn fetch_consumer_tasks(
        &self,
        stream_name: &str,
        group_name: &str,
        consumer_name: &str,
    ) -> CustomResult<Vec<storage::ProcessTracker>, ProcessTrackerError> {
        crate::consumer::fetch_consumer_tasks(
            self,
            &self
                .get_redis_conn()
                .map_err(ProcessTrackerError::ERedisError)?
                .clone(),
            stream_name,
            group_name,
            consumer_name,
        )
        .await
    }

    async fn consumer_group_create(
        &self,
        stream: &str,
        group: &str,
        id: &RedisEntryId,
    ) -> CustomResult<(), RedisError> {
        self.get_redis_conn()?
            .consumer_group_create(&stream.into(), group, id)
            .await
    }

    async fn acquire_pt_lock(
        &self,
        tag: &str,
        lock_key: &str,
        lock_val: &str,
        ttl: i64,
    ) -> CustomResult<bool, RedisError> {
        let conn = self.get_redis_conn()?.clone();
        let is_lock_acquired = conn
            .set_key_if_not_exists_with_expiry(&lock_key.into(), lock_val, None)
            .await;
        Ok(match is_lock_acquired {
            Ok(SetnxReply::KeySet) => match conn.set_expiry(&lock_key.into(), ttl).await {
                Ok(()) => true,

                #[allow(unused_must_use)]
                Err(error) => {
                    logger::error!(?error);
                    conn.delete_key(&lock_key.into()).await;
                    false
                }
            },
            Ok(SetnxReply::KeyNotSet) => {
                logger::error!(%tag, "Lock not acquired, previous fetch still in progress");
                false
            }
            Err(error) => {
                logger::error!(?error, %tag, "Error while locking");
                false
            }
        })
    }

    async fn release_pt_lock(&self, tag: &str, lock_key: &str) -> CustomResult<bool, RedisError> {
        let is_lock_released = self.get_redis_conn()?.delete_key(&lock_key.into()).await;
        Ok(match is_lock_released {
            Ok(_del_reply) => true,
            Err(error) => {
                logger::error!(?error, %tag, "Error while releasing lock");
                false
            }
        })
    }

    async fn stream_append_entry(
        &self,
        stream: &str,
        entry_id: &RedisEntryId,
        fields: Vec<(&str, String)>,
    ) -> CustomResult<(), RedisError> {
        self.get_redis_conn()?
            .stream_append_entry(&stream.into(), entry_id, fields)
            .await
    }

    async fn get_key(&self, key: &str) -> CustomResult<Vec<u8>, RedisError> {
        self.get_redis_conn()?.get_key::<Vec<u8>>(&key.into()).await
    }
}

#[async_trait::async_trait]
impl QueueInterface for MockDb {
    async fn fetch_consumer_tasks(
        &self,
        _stream_name: &str,
        _group_name: &str,
        _consumer_name: &str,
    ) -> CustomResult<Vec<storage::ProcessTracker>, ProcessTrackerError> {
        // [#172]: Implement function for `MockDb`
        Err(ProcessTrackerError::ResourceFetchingFailed {
            resource_name: "consumer_tasks".to_string(),
        })?
    }

    async fn consumer_group_create(
        &self,
        _stream: &str,
        _group: &str,
        _id: &RedisEntryId,
    ) -> CustomResult<(), RedisError> {
        // [#172]: Implement function for `MockDb`
        Err(RedisError::ConsumerGroupCreateFailed)?
    }

    async fn acquire_pt_lock(
        &self,
        _tag: &str,
        _lock_key: &str,
        _lock_val: &str,
        _ttl: i64,
    ) -> CustomResult<bool, RedisError> {
        // [#172]: Implement function for `MockDb`
        Ok(false)
    }

    async fn release_pt_lock(&self, _tag: &str, _lock_key: &str) -> CustomResult<bool, RedisError> {
        // [#172]: Implement function for `MockDb`
        Ok(false)
    }

    async fn stream_append_entry(
        &self,
        _stream: &str,
        _entry_id: &RedisEntryId,
        _fields: Vec<(&str, String)>,
    ) -> CustomResult<(), RedisError> {
        // [#172]: Implement function for `MockDb`
        Err(RedisError::StreamAppendFailed)?
    }

    async fn get_key(&self, _key: &str) -> CustomResult<Vec<u8>, RedisError> {
        Err(RedisError::RedisConnectionError.into())
    }
}


// File: crates/scheduler/src/db/process_tracker.rs

use common_utils::errors::CustomResult;
pub use diesel_models as storage;
use diesel_models::enums as storage_enums;
use error_stack::{report, ResultExt};
use storage_impl::{connection, errors, mock_db::MockDb};
use time::PrimitiveDateTime;

use crate::{metrics, scheduler::Store};

#[async_trait::async_trait]
pub trait ProcessTrackerInterface: Send + Sync + 'static {
    async fn reinitialize_limbo_processes(
        &self,
        ids: Vec<String>,
        schedule_time: PrimitiveDateTime,
    ) -> CustomResult<usize, errors::StorageError>;

    async fn find_process_by_id(
        &self,
        id: &str,
    ) -> CustomResult<Option<storage::ProcessTracker>, errors::StorageError>;

    async fn update_process(
        &self,
        this: storage::ProcessTracker,
        process: storage::ProcessTrackerUpdate,
    ) -> CustomResult<storage::ProcessTracker, errors::StorageError>;

    async fn process_tracker_update_process_status_by_ids(
        &self,
        task_ids: Vec<String>,
        task_update: storage::ProcessTrackerUpdate,
    ) -> CustomResult<usize, errors::StorageError>;

    async fn insert_process(
        &self,
        new: storage::ProcessTrackerNew,
    ) -> CustomResult<storage::ProcessTracker, errors::StorageError>;

    async fn reset_process(
        &self,
        this: storage::ProcessTracker,
        schedule_time: PrimitiveDateTime,
    ) -> CustomResult<(), errors::StorageError>;

    async fn retry_process(
        &self,
        this: storage::ProcessTracker,
        schedule_time: PrimitiveDateTime,
    ) -> CustomResult<(), errors::StorageError>;

    async fn finish_process_with_business_status(
        &self,
        this: storage::ProcessTracker,
        business_status: &'static str,
    ) -> CustomResult<(), errors::StorageError>;

    async fn find_processes_by_time_status(
        &self,
        time_lower_limit: PrimitiveDateTime,
        time_upper_limit: PrimitiveDateTime,
        status: storage_enums::ProcessTrackerStatus,
        limit: Option<i64>,
    ) -> CustomResult<Vec<storage::ProcessTracker>, errors::StorageError>;
}

#[async_trait::async_trait]
impl ProcessTrackerInterface for Store {
    async fn find_process_by_id(
        &self,
        id: &str,
    ) -> CustomResult<Option<storage::ProcessTracker>, errors::StorageError> {
        let conn = connection::pg_connection_read(self).await?;
        storage::ProcessTracker::find_process_by_id(&conn, id)
            .await
            .map_err(|error| report!(errors::StorageError::from(error)))
    }

    async fn reinitialize_limbo_processes(
        &self,
        ids: Vec<String>,
        schedule_time: PrimitiveDateTime,
    ) -> CustomResult<usize, errors::StorageError> {
        let conn = connection::pg_connection_write(self).await?;
        storage::ProcessTracker::reinitialize_limbo_processes(&conn, ids, schedule_time)
            .await
            .map_err(|error| report!(errors::StorageError::from(error)))
    }

    async fn find_processes_by_time_status(
        &self,
        time_lower_limit: PrimitiveDateTime,
        time_upper_limit: PrimitiveDateTime,
        status: storage_enums::ProcessTrackerStatus,
        limit: Option<i64>,
    ) -> CustomResult<Vec<storage::ProcessTracker>, errors::StorageError> {
        let conn = connection::pg_connection_read(self).await?;
        storage::ProcessTracker::find_processes_by_time_status(
            &conn,
            time_lower_limit,
            time_upper_limit,
            status,
            limit,
            common_types::consts::API_VERSION,
        )
        .await
        .map_err(|error| report!(errors::StorageError::from(error)))
    }

    async fn insert_process(
        &self,
        new: storage::ProcessTrackerNew,
    ) -> CustomResult<storage::ProcessTracker, errors::StorageError> {
        let conn = connection::pg_connection_write(self).await?;
        new.insert_process(&conn)
            .await
            .map_err(|error| report!(errors::StorageError::from(error)))
    }

    async fn update_process(
        &self,
        this: storage::ProcessTracker,
        process: storage::ProcessTrackerUpdate,
    ) -> CustomResult<storage::ProcessTracker, errors::StorageError> {
        let conn = connection::pg_connection_write(self).await?;
        this.update(&conn, process)
            .await
            .map_err(|error| report!(errors::StorageError::from(error)))
    }

    async fn reset_process(
        &self,
        this: storage::ProcessTracker,
        schedule_time: PrimitiveDateTime,
    ) -> CustomResult<(), errors::StorageError> {
        self.update_process(
            this,
            storage::ProcessTrackerUpdate::StatusRetryUpdate {
                status: storage_enums::ProcessTrackerStatus::New,
                retry_count: 0,
                schedule_time,
            },
        )
        .await?;
        Ok(())
    }

    async fn retry_process(
        &self,
        this: storage::ProcessTracker,
        schedule_time: PrimitiveDateTime,
    ) -> CustomResult<(), errors::StorageError> {
        metrics::TASK_RETRIED.add(1, &[]);
        let retry_count = this.retry_count + 1;
        self.update_process(
            this,
            storage::ProcessTrackerUpdate::StatusRetryUpdate {
                status: storage_enums::ProcessTrackerStatus::Pending,
                retry_count,
                schedule_time,
            },
        )
        .await?;
        Ok(())
    }

    async fn finish_process_with_business_status(
        &self,
        this: storage::ProcessTracker,
        business_status: &'static str,
    ) -> CustomResult<(), errors::StorageError> {
        self.update_process(
            this,
            storage::ProcessTrackerUpdate::StatusUpdate {
                status: storage_enums::ProcessTrackerStatus::Finish,
                business_status: Some(String::from(business_status)),
            },
        )
        .await
        .attach_printable("Failed to update business status of process")?;
        metrics::TASK_FINISHED.add(1, &[]);
        Ok(())
    }

    async fn process_tracker_update_process_status_by_ids(
        &self,
        task_ids: Vec<String>,
        task_update: storage::ProcessTrackerUpdate,
    ) -> CustomResult<usize, errors::StorageError> {
        let conn = connection::pg_connection_write(self).await?;
        storage::ProcessTracker::update_process_status_by_ids(&conn, task_ids, task_update)
            .await
            .map_err(|error| report!(errors::StorageError::from(error)))
    }
}

#[async_trait::async_trait]
impl ProcessTrackerInterface for MockDb {
    async fn find_process_by_id(
        &self,
        id: &str,
    ) -> CustomResult<Option<storage::ProcessTracker>, errors::StorageError> {
        let optional = self
            .processes
            .lock()
            .await
            .iter()
            .find(|process| process.id == id)
            .cloned();

        Ok(optional)
    }

    async fn reinitialize_limbo_processes(
        &self,
        _ids: Vec<String>,
        _schedule_time: PrimitiveDateTime,
    ) -> CustomResult<usize, errors::StorageError> {
        // [#172]: Implement function for `MockDb`
        Err(errors::StorageError::MockDbError)?
    }

    async fn find_processes_by_time_status(
        &self,
        _time_lower_limit: PrimitiveDateTime,
        _time_upper_limit: PrimitiveDateTime,
        _status: storage_enums::ProcessTrackerStatus,
        _limit: Option<i64>,
    ) -> CustomResult<Vec<storage::ProcessTracker>, errors::StorageError> {
        // [#172]: Implement function for `MockDb`
        Err(errors::StorageError::MockDbError)?
    }
    async fn insert_process(
        &self,
        new: storage::ProcessTrackerNew,
    ) -> CustomResult<storage::ProcessTracker, errors::StorageError> {
        let mut processes = self.processes.lock().await;
        let process = storage::ProcessTracker {
            id: new.id,
            name: new.name,
            tag: new.tag,
            runner: new.runner,
            retry_count: new.retry_count,
            schedule_time: new.schedule_time,
            rule: new.rule,
            tracking_data: new.tracking_data,
            business_status: new.business_status,
            status: new.status,
            event: new.event,
            created_at: new.created_at,
            updated_at: new.updated_at,
            version: new.version,
        };
        processes.push(process.clone());
        Ok(process)
    }

    async fn update_process(
        &self,
        _this: storage::ProcessTracker,
        _process: storage::ProcessTrackerUpdate,
    ) -> CustomResult<storage::ProcessTracker, errors::StorageError> {
        // [#172]: Implement function for `MockDb`
        Err(errors::StorageError::MockDbError)?
    }

    async fn reset_process(
        &self,
        _this: storage::ProcessTracker,
        _schedule_time: PrimitiveDateTime,
    ) -> CustomResult<(), errors::StorageError> {
        // [#172]: Implement function for `MockDb`
        Err(errors::StorageError::MockDbError)?
    }

    async fn retry_process(
        &self,
        _this: storage::ProcessTracker,
        _schedule_time: PrimitiveDateTime,
    ) -> CustomResult<(), errors::StorageError> {
        // [#172]: Implement function for `MockDb`
        Err(errors::StorageError::MockDbError)?
    }

    async fn finish_process_with_business_status(
        &self,
        _this: storage::ProcessTracker,
        _business_status: &'static str,
    ) -> CustomResult<(), errors::StorageError> {
        // [#172]: Implement function for `MockDb`
        Err(errors::StorageError::MockDbError)?
    }

    async fn process_tracker_update_process_status_by_ids(
        &self,
        _task_ids: Vec<String>,
        _task_update: storage::ProcessTrackerUpdate,
    ) -> CustomResult<usize, errors::StorageError> {
        // [#172]: Implement function for `MockDb`
        Err(errors::StorageError::MockDbError)?
    }
}


// File: crates/scheduler/src/consumer/types.rs

pub mod batch;
pub mod process_data;

pub use self::batch::ProcessTrackerBatch;


// File: crates/scheduler/src/consumer/workflows.rs

use async_trait::async_trait;
use common_utils::errors::CustomResult;
pub use diesel_models::process_tracker as storage;
use diesel_models::process_tracker::business_status;
use router_env::logger;

use crate::{errors, SchedulerSessionState};

pub type WorkflowSelectorFn =
    fn(&storage::ProcessTracker) -> Result<(), errors::ProcessTrackerError>;

#[async_trait]
pub trait ProcessTrackerWorkflows<T>: Send + Sync {
    // The core execution of the workflow
    async fn trigger_workflow<'a>(
        &'a self,
        _state: &'a T,
        _process: storage::ProcessTracker,
    ) -> CustomResult<(), errors::ProcessTrackerError> {
        Err(errors::ProcessTrackerError::NotImplemented)?
    }

    async fn execute_workflow<'a>(
        &'a self,
        operation: Box<dyn ProcessTrackerWorkflow<T>>,
        state: &'a T,
        process: storage::ProcessTracker,
    ) -> CustomResult<(), errors::ProcessTrackerError>
    where
        T: SchedulerSessionState,
    {
        let app_state = &state.clone();
        let output = operation.execute_workflow(app_state, process.clone()).await;
        match output {
            Ok(_) => operation.success_handler(app_state, process).await,
            Err(error) => match operation
                .error_handler(app_state, process.clone(), error)
                .await
            {
                Ok(_) => (),
                Err(error) => {
                    logger::error!(
                        ?error,
                        "Failed to handle process tracker workflow execution error"
                    );
                    let status = app_state
                        .get_db()
                        .as_scheduler()
                        .finish_process_with_business_status(
                            process,
                            business_status::GLOBAL_FAILURE,
                        )
                        .await;
                    if let Err(error) = status {
                        logger::error!(?error, "Failed to update process business status");
                    }
                }
            },
        };
        Ok(())
    }
}

#[async_trait]
pub trait ProcessTrackerWorkflow<T>: Send + Sync {
    /// The core execution of the workflow
    async fn execute_workflow<'a>(
        &'a self,
        _state: &'a T,
        _process: storage::ProcessTracker,
    ) -> Result<(), errors::ProcessTrackerError> {
        Err(errors::ProcessTrackerError::NotImplemented)?
    }

    /// Callback function after successful execution of the `execute_workflow`
    async fn success_handler<'a>(&'a self, _state: &'a T, _process: storage::ProcessTracker) {}

    /// Callback function after error received from `execute_workflow`
    async fn error_handler<'a>(
        &'a self,
        _state: &'a T,
        _process: storage::ProcessTracker,
        _error: errors::ProcessTrackerError,
    ) -> CustomResult<(), errors::ProcessTrackerError> {
        Err(errors::ProcessTrackerError::NotImplemented)?
    }
}


// File: crates/scheduler/src/consumer/types/batch.rs

use std::collections::HashMap;

use common_utils::{errors::CustomResult, ext_traits::OptionExt};
use diesel_models::process_tracker::ProcessTracker;
use error_stack::ResultExt;
use time::PrimitiveDateTime;

use crate::errors;

#[derive(Debug, Clone)]
pub struct ProcessTrackerBatch {
    pub id: String,
    pub group_name: String,
    pub stream_name: String,
    pub connection_name: String,
    pub created_time: PrimitiveDateTime,
    pub rule: String,                  // is it required?
    pub trackers: Vec<ProcessTracker>, /* FIXME: Add sized also here,  list */
}

impl ProcessTrackerBatch {
    pub fn to_redis_field_value_pairs(
        &self,
    ) -> CustomResult<Vec<(&str, String)>, errors::ProcessTrackerError> {
        Ok(vec![
            ("id", self.id.to_string()),
            ("group_name", self.group_name.to_string()),
            ("stream_name", self.stream_name.to_string()),
            ("connection_name", self.connection_name.to_string()),
            (
                "created_time",
                self.created_time.assume_utc().unix_timestamp().to_string(),
            ),
            ("rule", self.rule.to_string()),
            (
                "trackers",
                serde_json::to_string(&self.trackers)
                    .change_context(errors::ProcessTrackerError::SerializationFailed)
                    .attach_printable_lazy(|| {
                        format!("Unable to stringify trackers: {:?}", self.trackers)
                    })?,
            ),
        ])
    }

    pub fn from_redis_stream_entry(
        entry: HashMap<String, Option<String>>,
    ) -> CustomResult<Self, errors::ProcessTrackerError> {
        let mut entry = entry;
        let id = entry
            .remove("id")
            .flatten()
            .get_required_value("id")
            .change_context(errors::ProcessTrackerError::MissingRequiredField)?;
        let group_name = entry
            .remove("group_name")
            .flatten()
            .get_required_value("group_name")
            .change_context(errors::ProcessTrackerError::MissingRequiredField)?;
        let stream_name = entry
            .remove("stream_name")
            .flatten()
            .get_required_value("stream_name")
            .change_context(errors::ProcessTrackerError::MissingRequiredField)?;
        let connection_name = entry
            .remove("connection_name")
            .flatten()
            .get_required_value("connection_name")
            .change_context(errors::ProcessTrackerError::MissingRequiredField)?;
        let created_time = entry
            .remove("created_time")
            .flatten()
            .get_required_value("created_time")
            .change_context(errors::ProcessTrackerError::MissingRequiredField)?;

        //make it parser error
        let created_time = {
            let offset_date_time = time::OffsetDateTime::from_unix_timestamp(
                created_time
                    .as_str()
                    .parse::<i64>()
                    .change_context(errors::ParsingError::UnknownError)
                    .change_context(errors::ProcessTrackerError::DeserializationFailed)?,
            )
            .attach_printable_lazy(|| format!("Unable to parse time {}", &created_time))
            .change_context(errors::ProcessTrackerError::MissingRequiredField)?;
            PrimitiveDateTime::new(offset_date_time.date(), offset_date_time.time())
        };

        let rule = entry
            .remove("rule")
            .flatten()
            .get_required_value("rule")
            .change_context(errors::ProcessTrackerError::MissingRequiredField)?;
        let trackers = entry
            .remove("trackers")
            .flatten()
            .get_required_value("trackers")
            .change_context(errors::ProcessTrackerError::MissingRequiredField)?;

        let trackers = serde_json::from_str::<Vec<ProcessTracker>>(trackers.as_str())
            .change_context(errors::ParsingError::UnknownError)
            .attach_printable_lazy(|| {
                format!("Unable to parse trackers from JSON string: {trackers:?}")
            })
            .change_context(errors::ProcessTrackerError::DeserializationFailed)
            .attach_printable("Error parsing ProcessTracker from redis stream entry")?;

        Ok(Self {
            id,
            group_name,
            stream_name,
            connection_name,
            created_time,
            rule,
            trackers,
        })
    }
}


// File: crates/scheduler/src/consumer/types/process_data.rs

use std::collections::HashMap;

use diesel_models::enums;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct RetryMapping {
    pub start_after: i32,
    pub frequencies: Vec<(i32, i32)>, // (frequency, count)
}

#[derive(Serialize, Deserialize)]
pub struct ConnectorPTMapping {
    pub default_mapping: RetryMapping,
    pub custom_merchant_mapping: HashMap<common_utils::id_type::MerchantId, RetryMapping>,
    pub max_retries_count: i32,
}

impl Default for ConnectorPTMapping {
    fn default() -> Self {
        Self {
            custom_merchant_mapping: HashMap::new(),
            default_mapping: RetryMapping {
                start_after: 60,
                frequencies: vec![(300, 5)],
            },
            max_retries_count: 5,
        }
    }
}

#[derive(Serialize, Deserialize)]
pub struct SubscriptionInvoiceSyncPTMapping {
    pub default_mapping: RetryMapping,
    pub custom_merchant_mapping: HashMap<common_utils::id_type::MerchantId, RetryMapping>,
    pub max_retries_count: i32,
}

impl Default for SubscriptionInvoiceSyncPTMapping {
    fn default() -> Self {
        Self {
            custom_merchant_mapping: HashMap::new(),
            default_mapping: RetryMapping {
                start_after: 60,
                frequencies: vec![(300, 5)],
            },
            max_retries_count: 5,
        }
    }
}

#[derive(Serialize, Deserialize)]
pub struct PaymentMethodsPTMapping {
    pub default_mapping: RetryMapping,
    pub custom_pm_mapping: HashMap<enums::PaymentMethod, RetryMapping>,
    pub max_retries_count: i32,
}

impl Default for PaymentMethodsPTMapping {
    fn default() -> Self {
        Self {
            custom_pm_mapping: HashMap::new(),
            default_mapping: RetryMapping {
                start_after: 900,
                frequencies: vec![(300, 5)],
            },
            max_retries_count: 5,
        }
    }
}

/// Configuration for outgoing webhook retries.
#[derive(Debug, Serialize, Deserialize)]
pub struct OutgoingWebhookRetryProcessTrackerMapping {
    /// Default (fallback) retry configuration used when no merchant-specific retry configuration
    /// exists.
    pub default_mapping: RetryMapping,

    /// Merchant-specific retry configuration.
    pub custom_merchant_mapping: HashMap<common_utils::id_type::MerchantId, RetryMapping>,
}

impl Default for OutgoingWebhookRetryProcessTrackerMapping {
    fn default() -> Self {
        Self {
            default_mapping: RetryMapping {
                // 1st attempt happens after 1 minute
                start_after: 60,

                frequencies: vec![
                    // 2nd and 3rd attempts happen at intervals of 5 minutes each
                    (60 * 5, 2),
                    // 4th, 5th, 6th, 7th and 8th attempts happen at intervals of 10 minutes each
                    (60 * 10, 5),
                    // 9th, 10th, 11th, 12th and 13th attempts happen at intervals of 1 hour each
                    (60 * 60, 5),
                    // 14th, 15th and 16th attempts happen at intervals of 6 hours each
                    (60 * 60 * 6, 3),
                ],
            },
            custom_merchant_mapping: HashMap::new(),
        }
    }
}

/// Configuration for outgoing webhook retries.
#[derive(Debug, Serialize, Deserialize)]
pub struct RevenueRecoveryPaymentProcessTrackerMapping {
    /// Default (fallback) retry configuration used when no merchant-specific retry configuration
    /// exists.
    pub default_mapping: RetryMapping,

    /// Merchant-specific retry configuration.
    pub custom_merchant_mapping: HashMap<common_utils::id_type::MerchantId, RetryMapping>,
}

impl Default for RevenueRecoveryPaymentProcessTrackerMapping {
    fn default() -> Self {
        Self {
            default_mapping: RetryMapping {
                // 1st attempt happens after 1 minute of it being
                start_after: 60,

                frequencies: vec![
                    // 2nd and 3rd attempts happen at intervals of 3 hours each
                    (60 * 60 * 3, 2),
                    // 4th, 5th, 6th attempts happen at intervals of 6 hours each
                    (60 * 60 * 6, 3),
                    // 7th, 8th, 9th attempts happen at intervals of 9 hour each
                    (60 * 60 * 9, 3),
                    // 10th, 11th and 12th attempts happen at intervals of 12 hours each
                    (60 * 60 * 12, 3),
                    // 13th, 14th and 15th attempts happen at intervals of 18 hours each
                    (60 * 60 * 18, 3),
                ],
            },
            custom_merchant_mapping: HashMap::new(),
        }
    }
}


// File: crates/smithy-generator/Cargo.toml

[package]
name = "smithy-generator"
version = "0.1.0"
edition.workspace = true
rust-version.workspace = true
license.workspace = true
build = "build.rs"

[features]
default = ["v1"]
v1 = ["api_models/v1", "common_utils/v1"]
v2 = ["api_models/v2", "common_utils/v2"]

[dependencies]
smithy-core = { path = "../smithy-core" }
api_models = { version = "0.1.0", path = "../api_models" }
common_enums = { version = "0.1.0", path = "../common_enums" }
common_types = { version = "0.1.0", path = "../common_types" }
common_utils = { version = "0.1.0", path = "../common_utils" }
router_env = { version = "0.1.0", path = "../router_env" }

[build-dependencies]
regex = "1"

[lints]
workspace = true