diff --git a/crates/diesel_models/src/api_keys.rs b/crates/diesel_models/src/api_keys.rs index 6608d4ed2f..1875fc4dc5 100644 --- a/crates/diesel_models/src/api_keys.rs +++ b/crates/diesel_models/src/api_keys.rs @@ -138,7 +138,7 @@ mod diesel_impl { // Tracking data by process_tracker #[derive(Default, Debug, Deserialize, Serialize, Clone)] -pub struct ApiKeyExpiryWorkflow { +pub struct ApiKeyExpiryTrackingData { pub key_id: String, pub merchant_id: String, pub api_key_expiry: Option, diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 7f724729d9..c8bdb5b63f 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -13,7 +13,7 @@ default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "acc aws_s3 = ["external_services/aws_s3"] aws_kms = ["external_services/aws_kms"] hashicorp-vault = ["external_services/hashicorp-vault"] -email = ["external_services/email", "olap"] +email = ["external_services/email", "scheduler/email", "olap"] frm = [] stripe = ["dep:serde_qs"] release = ["aws_kms", "stripe", "aws_s3", "email", "backwards_compatibility", "business_profile_routing", "accounts_cache", "kv_store", "connector_choice_mca_id", "profile_specific_fallback_routing", "vergen", "recon"] diff --git a/crates/router/src/bin/scheduler.rs b/crates/router/src/bin/scheduler.rs index 59a108ef5e..caa69ea139 100644 --- a/crates/router/src/bin/scheduler.rs +++ b/crates/router/src/bin/scheduler.rs @@ -216,6 +216,8 @@ pub enum PTRunner { PaymentsSyncWorkflow, RefundWorkflowRouter, DeleteTokenizeDataWorkflow, + #[cfg(feature = "email")] + ApiKeyExpiryWorkflow, } #[derive(Debug, Copy, Clone)] @@ -240,6 +242,10 @@ impl ProcessTrackerWorkflows for WorkflowRunner { Some(PTRunner::DeleteTokenizeDataWorkflow) => { Box::new(workflows::tokenized_data::DeleteTokenizeDataWorkflow) } + #[cfg(feature = "email")] + Some(PTRunner::ApiKeyExpiryWorkflow) => { + Box::new(workflows::api_key_expiry::ApiKeyExpiryWorkflow) + } _ => Err(ProcessTrackerError::UnexpectedFlow)?, }; let app_state = &state.clone(); diff --git a/crates/router/src/core/api_keys.rs b/crates/router/src/core/api_keys.rs index b694d1291a..39212ab381 100644 --- a/crates/router/src/core/api_keys.rs +++ b/crates/router/src/core/api_keys.rs @@ -253,7 +253,7 @@ pub async fn add_api_key_expiry_task( } } - let api_key_expiry_tracker = &storage::ApiKeyExpiryWorkflow { + let api_key_expiry_tracker = &storage::ApiKeyExpiryTrackingData { key_id: api_key.key_id.clone(), merchant_id: api_key.merchant_id.clone(), // We need API key expiry too, because we need to decide on the schedule_time in @@ -427,7 +427,7 @@ pub async fn update_api_key_expiry_task( let task_ids = vec![task_id.clone()]; - let updated_tracking_data = &storage::ApiKeyExpiryWorkflow { + let updated_tracking_data = &storage::ApiKeyExpiryTrackingData { key_id: api_key.key_id.clone(), merchant_id: api_key.merchant_id.clone(), api_key_expiry: api_key.expires_at, diff --git a/crates/router/src/services/email/assets/api_key_expiry_reminder.html b/crates/router/src/services/email/assets/api_key_expiry_reminder.html new file mode 100644 index 0000000000..1865ae3814 --- /dev/null +++ b/crates/router/src/services/email/assets/api_key_expiry_reminder.html @@ -0,0 +1,203 @@ + +API Key Expiry Notice + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Dear Merchant,

+ + It has come to our attention that your API key will expire in {expires_in} days. To ensure uninterrupted + access to our platform and continued smooth operation of your services, we kindly request that you take the + necessary actions as soon as possible. + +
+ Thanks,
+ Team Hyperswitch +
+
+ diff --git a/crates/router/src/services/email/types.rs b/crates/router/src/services/email/types.rs index 6ad1a0eb99..389979d572 100644 --- a/crates/router/src/services/email/types.rs +++ b/crates/router/src/services/email/types.rs @@ -44,6 +44,9 @@ pub enum EmailBody { user_name: String, user_email: String, }, + ApiKeyExpiryReminder { + expires_in: u8, + }, } pub mod html { @@ -113,6 +116,10 @@ Email : {user_email} (note: This is an auto generated email. Use merchant email for any further communications)", ), + EmailBody::ApiKeyExpiryReminder { expires_in } => format!( + include_str!("assets/api_key_expiry_reminder.html"), + expires_in = expires_in, + ), } } } @@ -381,3 +388,26 @@ impl EmailData for ProFeatureRequest { }) } } + +pub struct ApiKeyExpiryReminder { + pub recipient_email: domain::UserEmail, + pub subject: &'static str, + pub expires_in: u8, +} + +#[async_trait::async_trait] +impl EmailData for ApiKeyExpiryReminder { + async fn get_email_data(&self) -> CustomResult { + let recipient = self.recipient_email.clone().into_inner(); + + let body = html::get_html_body(EmailBody::ApiKeyExpiryReminder { + expires_in: self.expires_in, + }); + + Ok(EmailContents { + subject: self.subject.to_string(), + body: external_services::email::IntermediateString::new(body), + recipient, + }) + } +} diff --git a/crates/router/src/types/storage/api_keys.rs b/crates/router/src/types/storage/api_keys.rs index 74c503d3c7..b1051c3d19 100644 --- a/crates/router/src/types/storage/api_keys.rs +++ b/crates/router/src/types/storage/api_keys.rs @@ -1,3 +1,3 @@ #[cfg(feature = "email")] -pub use diesel_models::api_keys::ApiKeyExpiryWorkflow; +pub use diesel_models::api_keys::ApiKeyExpiryTrackingData; pub use diesel_models::api_keys::{ApiKey, ApiKeyNew, ApiKeyUpdate, HashedApiKey}; diff --git a/crates/router/src/workflows.rs b/crates/router/src/workflows.rs index b036193bb2..deb5bf785f 100644 --- a/crates/router/src/workflows.rs +++ b/crates/router/src/workflows.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "email")] +pub mod api_key_expiry; pub mod payment_sync; pub mod refund_router; pub mod tokenized_data; diff --git a/crates/router/src/workflows/api_key_expiry.rs b/crates/router/src/workflows/api_key_expiry.rs index eb3c1d9c1c..b9830c4ebc 100644 --- a/crates/router/src/workflows/api_key_expiry.rs +++ b/crates/router/src/workflows/api_key_expiry.rs @@ -1,30 +1,35 @@ -use common_utils::ext_traits::ValueExt; -use diesel_models::enums::{self as storage_enums}; +use common_utils::{errors::ValidationError, ext_traits::ValueExt}; +use diesel_models::{enums as storage_enums, ApiKeyExpiryTrackingData}; +use router_env::logger; +use scheduler::{workflows::ProcessTrackerWorkflow, SchedulerAppState}; -use super::{ApiKeyExpiryWorkflow, ProcessTrackerWorkflow}; use crate::{ errors, logger::error, - routes::AppState, + routes::{metrics, AppState}, + services::email::types::ApiKeyExpiryReminder, types::{ api, + domain::UserEmail, storage::{self, ProcessTrackerExt}, }, utils::OptionExt, }; +pub struct ApiKeyExpiryWorkflow; + #[async_trait::async_trait] -impl ProcessTrackerWorkflow for ApiKeyExpiryWorkflow { +impl ProcessTrackerWorkflow for ApiKeyExpiryWorkflow { async fn execute_workflow<'a>( &'a self, state: &'a AppState, process: storage::ProcessTracker, ) -> Result<(), errors::ProcessTrackerError> { let db = &*state.store; - let tracking_data: storage::ApiKeyExpiryWorkflow = process + let tracking_data: ApiKeyExpiryTrackingData = process .tracking_data .clone() - .parse_value("ApiKeyExpiryWorkflow")?; + .parse_value("ApiKeyExpiryTrackingData")?; let key_store = state .store @@ -41,7 +46,13 @@ impl ProcessTrackerWorkflow for ApiKeyExpiryWorkflow { let email_id = merchant_account .merchant_details .parse_value::("MerchantDetails")? - .primary_email; + .primary_email + .ok_or(errors::ProcessTrackerError::EValidationError( + ValidationError::MissingRequiredField { + field_name: "email".to_string(), + } + .into(), + ))?; let task_id = process.id.clone(); @@ -53,28 +64,26 @@ impl ProcessTrackerWorkflow for ApiKeyExpiryWorkflow { usize::try_from(retry_count) .map_err(|_| errors::ProcessTrackerError::TypeConversionError)?, ) - .ok_or(errors::ProcessTrackerError::EApiErrorResponse( - errors::ApiErrorResponse::InvalidDataValue { - field_name: "index", - } - .into(), - ))?; + .ok_or(errors::ProcessTrackerError::EApiErrorResponse)?; + + let email_contents = ApiKeyExpiryReminder { + recipient_email: UserEmail::from_pii_email(email_id).map_err(|err| { + logger::error!(%err,"Failed to convert recipient's email to UserEmail from pii::Email"); + errors::ProcessTrackerError::EApiErrorResponse + })?, + subject: "API Key Expiry Notice", + expires_in: *expires_in, + }; state .email_client .clone() - .send_email( - email_id.ok_or_else(|| errors::ProcessTrackerError::MissingRequiredField)?, - "API Key Expiry Notice".to_string(), - format!("Dear Merchant,\n -It has come to our attention that your API key will expire in {expires_in} days. To ensure uninterrupted access to our platform and continued smooth operation of your services, we kindly request that you take the necessary actions as soon as possible.\n\n -Thanks,\n -Team Hyperswitch"), + .compose_and_send_email( + Box::new(email_contents), + state.conf.proxy.https_url.as_ref(), ) .await - .map_err(|_| errors::ProcessTrackerError::FlowExecutionError { - flow: "ApiKeyExpiryWorkflow", - })?; + .map_err(errors::ProcessTrackerError::EEmailError)?; // If all the mails have been sent, then retry_count would be equal to length of the expiry_reminder_days vector if retry_count @@ -82,7 +91,7 @@ Team Hyperswitch"), .map_err(|_| errors::ProcessTrackerError::TypeConversionError)? { process - .finish_with_status(db, format!("COMPLETED_BY_PT_{task_id}")) + .finish_with_status(state.get_db().as_scheduler(), "COMPLETED_BY_PT".to_string()) .await? } // If tasks are remaining that has to be scheduled @@ -93,12 +102,7 @@ Team Hyperswitch"), usize::try_from(retry_count + 1) .map_err(|_| errors::ProcessTrackerError::TypeConversionError)?, ) - .ok_or(errors::ProcessTrackerError::EApiErrorResponse( - errors::ApiErrorResponse::InvalidDataValue { - field_name: "index", - } - .into(), - ))?; + .ok_or(errors::ProcessTrackerError::EApiErrorResponse)?; let updated_schedule_time = tracking_data.api_key_expiry.map(|api_key_expiry| { api_key_expiry.saturating_sub(time::Duration::days(i64::from(*expiry_reminder_day))) diff --git a/crates/scheduler/Cargo.toml b/crates/scheduler/Cargo.toml index b281bc862a..72eaea3be0 100644 --- a/crates/scheduler/Cargo.toml +++ b/crates/scheduler/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" default = ["kv_store", "olap"] olap = ["storage_impl/olap"] kv_store = [] +email = ["external_services/email"] [dependencies] # Third party crates diff --git a/crates/scheduler/src/errors.rs b/crates/scheduler/src/errors.rs index 481fae0793..78a0bdee62 100644 --- a/crates/scheduler/src/errors.rs +++ b/crates/scheduler/src/errors.rs @@ -1,4 +1,6 @@ pub use common_utils::errors::{ParsingError, ValidationError}; +#[cfg(feature = "email")] +use external_services::email::EmailError; pub use redis_interface::errors::RedisError; pub use storage_impl::errors::ApplicationError; use storage_impl::errors::StorageError; @@ -51,6 +53,9 @@ pub enum ProcessTrackerError { EParsingError(error_stack::Report), #[error("Validation Error Received: {0}")] EValidationError(error_stack::Report), + #[cfg(feature = "email")] + #[error("Received Error EmailError: {0}")] + EEmailError(error_stack::Report), #[error("Type Conversion error")] TypeConversionError, } @@ -111,3 +116,9 @@ error_to_process_tracker_error!( error_stack::Report, ProcessTrackerError::EValidationError(error_stack::Report) ); + +#[cfg(feature = "email")] +error_to_process_tracker_error!( + error_stack::Report, + ProcessTrackerError::EEmailError(error_stack::Report) +);