mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-10-31 10:06:32 +08:00 
			
		
		
		
	refactor: include api key expiry workflow into process tracker (#3661)
This commit is contained in:
		| @ -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<PrimitiveDateTime>, | ||||
|  | ||||
| @ -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"] | ||||
|  | ||||
| @ -216,6 +216,8 @@ pub enum PTRunner { | ||||
|     PaymentsSyncWorkflow, | ||||
|     RefundWorkflowRouter, | ||||
|     DeleteTokenizeDataWorkflow, | ||||
|     #[cfg(feature = "email")] | ||||
|     ApiKeyExpiryWorkflow, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Copy, Clone)] | ||||
| @ -240,6 +242,10 @@ impl ProcessTrackerWorkflows<routes::AppState> 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(); | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -0,0 +1,203 @@ | ||||
| <meta content="text/html; charset=UTF-8" http-equiv="Content-Type" /> | ||||
| <title>API Key Expiry Notice</title> | ||||
| <body style="background-color: #ececec"> | ||||
|   <style> | ||||
|     .apple-footer a {{ | ||||
|       text-decoration: none !important; | ||||
|       color: #999 !important; | ||||
|       border: none !important; | ||||
|     }} | ||||
|     .apple-email a {{ | ||||
|       text-decoration: none !important; | ||||
|       color: #448bff !important; | ||||
|       border: none !important; | ||||
|     }} | ||||
|   </style> | ||||
|   <div | ||||
|     id="wrapper" | ||||
|     style=" | ||||
|       background-color: none; | ||||
|       margin: 0 auto; | ||||
|       text-align: center; | ||||
|       width: 60%; | ||||
|       -premailer-height: 200; | ||||
|     " | ||||
|   > | ||||
|     <table | ||||
|       align="center" | ||||
|       class="main-table" | ||||
|       style=" | ||||
|         -premailer-cellpadding: 0; | ||||
|         -premailer-cellspacing: 0; | ||||
|         background-color: #fff; | ||||
|         border: 0; | ||||
|         border-top: 5px solid #0165ef; | ||||
|         margin: 0 auto; | ||||
|         mso-table-lspace: 0; | ||||
|         mso-table-rspace: 0; | ||||
|         padding: 0 40; | ||||
|         text-align: center; | ||||
|         width: 100%; | ||||
|       " | ||||
|       bgcolor="#ffffff" | ||||
|       cellpadding="0" | ||||
|       cellspacing="0" | ||||
|     > | ||||
|       <tr> | ||||
|         <td | ||||
|           class="spacer-lg" | ||||
|           style=" | ||||
|             -premailer-height: 75; | ||||
|             -premailer-width: 100%; | ||||
|             line-height: 30px; | ||||
|             margin: 0 auto; | ||||
|             padding: 0; | ||||
|           " | ||||
|           height="75" | ||||
|           width="100%" | ||||
|         ></td> | ||||
|       </tr> | ||||
|       <tr> | ||||
|         <td | ||||
|           class="spacer-lg" | ||||
|           style=" | ||||
|             -premailer-height: 75; | ||||
|             -premailer-width: 100%; | ||||
|             line-height: 30px; | ||||
|             margin: 0 auto; | ||||
|             padding: 0; | ||||
|           " | ||||
|           height="25" | ||||
|           width="100%" | ||||
|         ></td> | ||||
|       </tr> | ||||
|       <tr> | ||||
|         <td | ||||
|           class="spacer-lg" | ||||
|           style=" | ||||
|             -premailer-height: 75; | ||||
|             -premailer-width: 100%; | ||||
|             line-height: 30px; | ||||
|             margin: 0 auto; | ||||
|             padding: 0; | ||||
|           " | ||||
|           height="50" | ||||
|           width="100%" | ||||
|         ></td> | ||||
|       </tr> | ||||
|       <tr> | ||||
|         <td | ||||
|           class="headline" | ||||
|           style=" | ||||
|             color: #444; | ||||
|             font-family: Roboto, Helvetica, Arial, san-serif; | ||||
|             font-size: 30px; | ||||
|             font-weight: 100; | ||||
|             line-height: 36px; | ||||
|             margin: 0 auto; | ||||
|             padding: 0; | ||||
|             text-align: center; | ||||
|           " | ||||
|           align="center" | ||||
|         > | ||||
|         <p style="font-size: 18px">Dear Merchant,</p> | ||||
|         <span style="font-size: 18px"> | ||||
|           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. | ||||
|         </span> | ||||
|         </td> | ||||
|       </tr> | ||||
|       <tr> | ||||
|         <td | ||||
|           class="spacer-sm" | ||||
|           style=" | ||||
|             -premailer-height: 20; | ||||
|             -premailer-width: 80%; | ||||
|             line-height: 10px; | ||||
|             margin: 0 auto; | ||||
|             padding: 0; | ||||
|           " | ||||
|           width="100%" | ||||
|         ></td> | ||||
|       </tr> | ||||
|        | ||||
|       <tr> | ||||
|         <td | ||||
|           class="spacer-sm" | ||||
|           style=" | ||||
|             -premailer-height: 20; | ||||
|             -premailer-width: 100%; | ||||
|             line-height: 10px; | ||||
|             margin: 0 auto; | ||||
|             padding: 0; | ||||
|           " | ||||
|           height="20" | ||||
|           width="100%" | ||||
|         ></td> | ||||
|       </tr> | ||||
|  | ||||
|       <tr> | ||||
|         <td | ||||
|           class="spacer-lg" | ||||
|           style=" | ||||
|             -premailer-height: 75; | ||||
|             -premailer-width: 100%; | ||||
|             line-height: 30px; | ||||
|             margin: 0 auto; | ||||
|             padding: 0; | ||||
|           " | ||||
|           height="75" | ||||
|           width="100%" | ||||
|         ></td> | ||||
|       </tr> | ||||
|       <tr> | ||||
|         <td | ||||
|           class="headline" | ||||
|           style=" | ||||
|             color: #444; | ||||
|             font-family: Roboto, Helvetica, Arial, san-serif; | ||||
|             font-size: 18px; | ||||
|             font-weight: 100; | ||||
|             line-height: 36px; | ||||
|             margin: 0 auto; | ||||
|             padding: 0; | ||||
|             text-align: center; | ||||
|           " | ||||
|           align="center" | ||||
|         > | ||||
|           Thanks,<br /> | ||||
|           Team Hyperswitch | ||||
|         </td> | ||||
|       </tr> | ||||
|       <tr> | ||||
|         <td | ||||
|           class="spacer-lg" | ||||
|           style=" | ||||
|             -premailer-height: 75; | ||||
|             -premailer-width: 100%; | ||||
|             line-height: 30px; | ||||
|             margin: 0 auto; | ||||
|             padding: 0; | ||||
|           " | ||||
|           height="75" | ||||
|           width="100%" | ||||
|         ></td> | ||||
|       </tr> | ||||
|       <tr> | ||||
|         <td | ||||
|           class="spacer-lg" | ||||
|           style=" | ||||
|             -premailer-height: 75; | ||||
|             -premailer-width: 100%; | ||||
|             line-height: 30px; | ||||
|             margin: 0 auto; | ||||
|             padding: 0; | ||||
|           " | ||||
|           height="75" | ||||
|           width="100%" | ||||
|         ></td> | ||||
|       </tr> | ||||
|     </table> | ||||
|   </div> | ||||
| </body> | ||||
| @ -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<EmailContents, EmailError> { | ||||
|         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, | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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}; | ||||
|  | ||||
| @ -1,3 +1,5 @@ | ||||
| #[cfg(feature = "email")] | ||||
| pub mod api_key_expiry; | ||||
| pub mod payment_sync; | ||||
| pub mod refund_router; | ||||
| pub mod tokenized_data; | ||||
|  | ||||
| @ -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<AppState> 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::<api::MerchantDetails>("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))) | ||||
|  | ||||
| @ -7,6 +7,7 @@ edition = "2021" | ||||
| default = ["kv_store", "olap"] | ||||
| olap = ["storage_impl/olap"] | ||||
| kv_store = [] | ||||
| email = ["external_services/email"] | ||||
|  | ||||
| [dependencies] | ||||
| # Third party crates | ||||
|  | ||||
| @ -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<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, | ||||
| } | ||||
| @ -111,3 +116,9 @@ 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>) | ||||
| ); | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Chethan Rao
					Chethan Rao