refactor: include api key expiry workflow into process tracker (#3661)

This commit is contained in:
Chethan Rao
2024-02-19 13:33:17 +05:30
committed by GitHub
parent d0f529fa4b
commit 0a7625ff8c
11 changed files with 293 additions and 36 deletions

View File

@ -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>,

View File

@ -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"]

View File

@ -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();

View File

@ -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,

View File

@ -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>

View File

@ -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,
})
}
}

View File

@ -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};

View File

@ -1,3 +1,5 @@
#[cfg(feature = "email")]
pub mod api_key_expiry;
pub mod payment_sync;
pub mod refund_router;
pub mod tokenized_data;

View File

@ -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)))

View File

@ -7,6 +7,7 @@ edition = "2021"
default = ["kv_store", "olap"]
olap = ["storage_impl/olap"]
kv_store = []
email = ["external_services/email"]
[dependencies]
# Third party crates

View File

@ -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>)
);