feat(recon): add recon APIs (#3345)

Co-authored-by: Kashif <mohammed.kashif@juspay.in>
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Kashif
2024-01-16 16:37:44 +05:30
committed by GitHub
parent 5ad3f8939a
commit 8678f8d144
22 changed files with 639 additions and 7 deletions

View File

@ -8,7 +8,7 @@ readme = "README.md"
license.workspace = true
[features]
default = ["payouts", "frm"]
default = ["payouts", "frm", "recon"]
business_profile_routing = []
connector_choice_bcompat = []
errors = ["dep:actix-web", "dep:reqwest"]
@ -18,6 +18,7 @@ dummy_connector = ["euclid/dummy_connector", "common_enums/dummy_connector"]
detailed_errors = []
payouts = []
frm = []
recon = []
[dependencies]
actix-web = { version = "4.3.1", optional = true }

View File

@ -5,6 +5,8 @@ mod locker_migration;
pub mod payment;
#[cfg(feature = "payouts")]
pub mod payouts;
#[cfg(feature = "recon")]
pub mod recon;
pub mod refund;
pub mod routing;
pub mod user;

View File

@ -0,0 +1,21 @@
use common_utils::events::{ApiEventMetric, ApiEventsType};
use crate::recon::{ReconStatusResponse, ReconTokenResponse, ReconUpdateMerchantRequest};
impl ApiEventMetric for ReconUpdateMerchantRequest {
fn get_api_event_type(&self) -> Option<ApiEventsType> {
Some(ApiEventsType::Recon)
}
}
impl ApiEventMetric for ReconTokenResponse {
fn get_api_event_type(&self) -> Option<ApiEventsType> {
Some(ApiEventsType::Recon)
}
}
impl ApiEventMetric for ReconStatusResponse {
fn get_api_event_type(&self) -> Option<ApiEventsType> {
Some(ApiEventsType::Recon)
}
}

View File

@ -1,7 +1,11 @@
use common_utils::events::{ApiEventMetric, ApiEventsType};
#[cfg(feature = "recon")]
use masking::PeekInterface;
#[cfg(feature = "dummy_connector")]
use crate::user::sample_data::SampleDataRequest;
#[cfg(feature = "recon")]
use crate::user::VerifyTokenResponse;
use crate::user::{
dashboard_metadata::{
GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest,
@ -21,6 +25,16 @@ impl ApiEventMetric for DashboardEntryResponse {
}
}
#[cfg(feature = "recon")]
impl ApiEventMetric for VerifyTokenResponse {
fn get_api_event_type(&self) -> Option<ApiEventsType> {
Some(ApiEventsType::User {
merchant_id: self.merchant_id.clone(),
user_id: self.user_email.peek().to_string(),
})
}
}
common_utils::impl_misc_api_event_type!(
SignUpRequest,
SignUpWithMerchantIdRequest,

View File

@ -26,6 +26,8 @@ pub mod payments;
#[cfg(feature = "payouts")]
pub mod payouts;
pub mod pm_auth;
#[cfg(feature = "recon")]
pub mod recon;
pub mod refunds;
pub mod routing;
pub mod surcharge_decision_configs;

View File

@ -0,0 +1,21 @@
use common_utils::pii;
use masking::Secret;
use crate::enums;
#[derive(serde::Deserialize, Debug, serde::Serialize)]
pub struct ReconUpdateMerchantRequest {
pub merchant_id: String,
pub recon_status: enums::ReconStatus,
pub user_email: pii::Email,
}
#[derive(Debug, serde::Serialize)]
pub struct ReconTokenResponse {
pub token: Secret<String>,
}
#[derive(Debug, serde::Serialize)]
pub struct ReconStatusResponse {
pub recon_status: enums::ReconStatus,
}

View File

@ -140,3 +140,10 @@ pub struct UserMerchantAccount {
pub merchant_id: String,
pub merchant_name: OptionalEncryptableName,
}
#[cfg(feature = "recon")]
#[derive(serde::Serialize, Debug)]
pub struct VerifyTokenResponse {
pub merchant_id: String,
pub user_email: pii::Email,
}

View File

@ -49,6 +49,7 @@ pub enum ApiEventsType {
Miscellaneous,
RustLocker,
FraudCheck,
Recon,
}
impl ApiEventMetric for serde_json::Value {}

View File

@ -9,7 +9,7 @@ readme = "README.md"
license.workspace = true
[features]
default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "business_profile_routing", "connector_choice_mca_id", "profile_specific_fallback_routing", "retry", "frm"]
default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "business_profile_routing", "connector_choice_mca_id", "profile_specific_fallback_routing", "retry", "frm", "recon"]
s3 = ["dep:aws-sdk-s3", "dep:aws-config"]
kms = ["external_services/kms", "dep:aws-config"]
email = ["external_services/email", "dep:aws-config", "olap"]
@ -30,6 +30,7 @@ connector_choice_mca_id = ["api_models/connector_choice_mca_id", "euclid/connect
external_access_dc = ["dummy_connector"]
detailed_errors = ["api_models/detailed_errors", "error-stack/serde"]
payouts = []
recon = ["email"]
retry = []
[dependencies]

View File

@ -757,3 +757,32 @@ pub async fn send_verification_mail(
Ok(ApplicationResponse::StatusOk)
}
#[cfg(feature = "recon")]
pub async fn verify_token(
state: AppState,
req: auth::ReconUser,
) -> UserResponse<user_api::VerifyTokenResponse> {
let user = state
.store
.find_user_by_id(&req.user_id)
.await
.map_err(|e| {
if e.current_context().is_db_not_found() {
e.change_context(UserErrors::UserNotFound)
} else {
e.change_context(UserErrors::InternalServerError)
}
})?;
let merchant_id = state
.store
.find_user_role_by_user_id(&req.user_id)
.await
.change_context(UserErrors::InternalServerError)?
.merchant_id;
Ok(ApplicationResponse::Json(user_api::VerifyTokenResponse {
merchant_id: merchant_id.to_string(),
user_email: user.email,
}))
}

View File

@ -165,6 +165,12 @@ pub fn mk_app(
{
server_app = server_app.service(routes::StripeApis::server(state.clone()));
}
#[cfg(feature = "recon")]
{
server_app = server_app.service(routes::Recon::server(state.clone()));
}
server_app = server_app.service(routes::Cards::server(state.clone()));
server_app = server_app.service(routes::Cache::server(state.clone()));
server_app = server_app.service(routes::Health::server(state));

View File

@ -28,6 +28,8 @@ pub mod payment_methods;
pub mod payments;
#[cfg(feature = "payouts")]
pub mod payouts;
#[cfg(feature = "recon")]
pub mod recon;
pub mod refunds;
#[cfg(feature = "olap")]
pub mod routing;
@ -53,6 +55,8 @@ pub use self::app::DummyConnector;
pub use self::app::Forex;
#[cfg(feature = "payouts")]
pub use self::app::Payouts;
#[cfg(all(feature = "olap", feature = "recon"))]
pub use self::app::Recon;
#[cfg(all(feature = "olap", feature = "kms"))]
pub use self::app::Verify;
pub use self::app::{

View File

@ -40,6 +40,8 @@ use super::{configs::*, customers::*, mandates::*, payments::*, refunds::*};
use super::{ephemeral_key::*, payment_methods::*, webhooks::*};
#[cfg(all(feature = "frm", feature = "oltp"))]
use crate::routes::fraud_check as frm_routes;
#[cfg(all(feature = "recon", feature = "olap"))]
use crate::routes::recon as recon_routes;
#[cfg(feature = "olap")]
use crate::routes::verify_connector::payment_connector_verify;
pub use crate::{
@ -568,6 +570,26 @@ impl PaymentMethods {
}
}
#[cfg(all(feature = "olap", feature = "recon"))]
pub struct Recon;
#[cfg(all(feature = "olap", feature = "recon"))]
impl Recon {
pub fn server(state: AppState) -> Scope {
web::scope("/recon")
.app_data(web::Data::new(state))
.service(
web::resource("/update_merchant")
.route(web::post().to(recon_routes::update_merchant)),
)
.service(web::resource("/token").route(web::get().to(recon_routes::get_recon_token)))
.service(
web::resource("/request").route(web::post().to(recon_routes::request_for_recon)),
)
.service(web::resource("/verify_token").route(web::get().to(verify_recon_token)))
}
}
#[cfg(feature = "olap")]
pub struct Blocklist;

View File

@ -31,6 +31,7 @@ pub enum ApiIdentifier {
User,
UserRole,
ConnectorOnboarding,
Recon,
}
impl From<Flow> for ApiIdentifier {
@ -186,6 +187,11 @@ impl From<Flow> for ApiIdentifier {
Flow::GetActionUrl | Flow::SyncOnboardingStatus | Flow::ResetTrackingId => {
Self::ConnectorOnboarding
}
Flow::ReconMerchantUpdate
| Flow::ReconTokenRequest
| Flow::ReconServiceRequest
| Flow::ReconVerifyToken => Self::Recon,
}
}
}

View File

@ -0,0 +1,250 @@
use actix_web::{web, HttpRequest, HttpResponse};
use api_models::recon as recon_api;
use common_enums::ReconStatus;
use error_stack::ResultExt;
use masking::{ExposeInterface, PeekInterface, Secret};
use router_env::Flow;
use super::AppState;
use crate::{
core::{
api_locking,
errors::{self, RouterResponse, RouterResult, StorageErrorExt, UserErrors},
},
services::{
api as service_api, api,
authentication::{self as auth, ReconUser, UserFromToken},
email::types as email_types,
recon::ReconToken,
},
types::{
api::{self as api_types, enums},
domain::{UserEmail, UserFromStorage, UserName},
storage,
},
};
pub async fn update_merchant(
state: web::Data<AppState>,
req: HttpRequest,
json_payload: web::Json<recon_api::ReconUpdateMerchantRequest>,
) -> HttpResponse {
let flow = Flow::ReconMerchantUpdate;
Box::pin(api::server_wrap(
flow,
state,
&req,
json_payload.into_inner(),
|state, _user, req| recon_merchant_account_update(state, req),
&auth::ReconAdmin,
api_locking::LockAction::NotApplicable,
))
.await
}
pub async fn request_for_recon(state: web::Data<AppState>, http_req: HttpRequest) -> HttpResponse {
let flow = Flow::ReconServiceRequest;
Box::pin(api::server_wrap(
flow,
state,
&http_req,
(),
|state, user: UserFromToken, _req| send_recon_request(state, user),
&auth::DashboardNoPermissionAuth,
api_locking::LockAction::NotApplicable,
))
.await
}
pub async fn get_recon_token(state: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
let flow = Flow::ReconTokenRequest;
Box::pin(api::server_wrap(
flow,
state,
&req,
(),
|state, user: ReconUser, _| generate_recon_token(state, user),
&auth::ReconJWT,
api_locking::LockAction::NotApplicable,
))
.await
}
pub async fn send_recon_request(
state: AppState,
user: UserFromToken,
) -> RouterResponse<recon_api::ReconStatusResponse> {
let db = &*state.store;
let user_from_db = db
.find_user_by_id(&user.user_id)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)?;
let merchant_id = db
.find_user_role_by_user_id(&user.user_id)
.await
.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?
.merchant_id;
let key_store = db
.get_merchant_key_store_by_merchant_id(
merchant_id.as_str(),
&db.get_master_key().to_vec().into(),
)
.await
.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?;
let merchant_account = db
.find_merchant_account_by_merchant_id(merchant_id.as_str(), &key_store)
.await
.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?;
let email_contents = email_types::ProFeatureRequest {
feature_name: "RECONCILIATION & SETTLEMENT".to_string(),
merchant_id: merchant_id.clone(),
user_name: UserName::new(user_from_db.name)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to form username")?,
recipient_email: UserEmail::from_pii_email(user_from_db.email.clone())
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to convert to UserEmail from pii::Email")?,
settings: state.conf.clone(),
subject: format!(
"Dashboard Pro Feature Request by {}",
user_from_db.email.expose().peek()
),
};
let is_email_sent = state
.email_client
.compose_and_send_email(
Box::new(email_contents),
state.conf.proxy.https_url.as_ref(),
)
.await
.change_context(UserErrors::InternalServerError)
.attach_printable("Failed to compose and send email for ProFeatureRequest")
.is_ok();
if is_email_sent {
let updated_merchant_account = storage::MerchantAccountUpdate::ReconUpdate {
recon_status: enums::ReconStatus::Requested,
};
let response = db
.update_merchant(merchant_account, updated_merchant_account, &key_store)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable_lazy(|| {
format!("Failed while updating merchant's recon status: {merchant_id}")
})?;
Ok(service_api::ApplicationResponse::Json(
recon_api::ReconStatusResponse {
recon_status: response.recon_status,
},
))
} else {
Ok(service_api::ApplicationResponse::Json(
recon_api::ReconStatusResponse {
recon_status: enums::ReconStatus::NotRequested,
},
))
}
}
pub async fn recon_merchant_account_update(
state: AppState,
req: recon_api::ReconUpdateMerchantRequest,
) -> RouterResponse<api_types::MerchantAccountResponse> {
let merchant_id = &req.merchant_id.clone();
let user_email = &req.user_email.clone();
let db = &*state.store;
let key_store = db
.get_merchant_key_store_by_merchant_id(
&req.merchant_id,
&db.get_master_key().to_vec().into(),
)
.await
.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?;
let merchant_account = db
.find_merchant_account_by_merchant_id(merchant_id, &key_store)
.await
.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?;
let updated_merchant_account = storage::MerchantAccountUpdate::ReconUpdate {
recon_status: req.recon_status,
};
let response = db
.update_merchant(merchant_account, updated_merchant_account, &key_store)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable_lazy(|| {
format!("Failed while updating merchant's recon status: {merchant_id}")
})?;
let email_contents = email_types::ReconActivation {
recipient_email: UserEmail::from_pii_email(user_email.clone())
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to convert to UserEmail from pii::Email")?,
user_name: UserName::new(Secret::new("HyperSwitch User".to_string()))
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to form username")?,
settings: state.conf.clone(),
subject: "Approval of Recon Request - Access Granted to Recon Dashboard",
};
if req.recon_status == ReconStatus::Active {
let _is_email_sent = state
.email_client
.compose_and_send_email(
Box::new(email_contents),
state.conf.proxy.https_url.as_ref(),
)
.await
.change_context(UserErrors::InternalServerError)
.attach_printable("Failed to compose and send email for ReconActivation")
.is_ok();
}
Ok(service_api::ApplicationResponse::Json(
response
.try_into()
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "merchant_account",
})?,
))
}
pub async fn generate_recon_token(
state: AppState,
req: ReconUser,
) -> RouterResponse<recon_api::ReconTokenResponse> {
let db = &*state.store;
let user = db
.find_user_by_id(&req.user_id)
.await
.map_err(|e| {
if e.current_context().is_db_not_found() {
e.change_context(errors::ApiErrorResponse::InvalidJwtToken)
} else {
e.change_context(errors::ApiErrorResponse::InternalServerError)
}
})?
.into();
let token = Box::pin(get_recon_auth_token(user, state))
.await
.change_context(errors::ApiErrorResponse::InternalServerError)?;
Ok(service_api::ApplicationResponse::Json(
recon_api::ReconTokenResponse { token },
))
}
pub async fn get_recon_auth_token(
user: UserFromStorage,
state: AppState,
) -> RouterResult<Secret<String>> {
ReconToken::new_token(user.0.user_id.clone(), &state.conf).await
}

View File

@ -388,3 +388,18 @@ pub async fn verify_email_request(
))
.await
}
#[cfg(feature = "recon")]
pub async fn verify_recon_token(state: web::Data<AppState>, http_req: HttpRequest) -> HttpResponse {
let flow = Flow::ReconVerifyToken;
Box::pin(api::server_wrap(
flow,
state.clone(),
&http_req,
(),
|state, user, _req| user_core::verify_token(state, user),
&auth::ReconJWT,
api_locking::LockAction::NotApplicable,
))
.await
}

View File

@ -7,6 +7,8 @@ pub mod jwt;
pub mod kafka;
pub mod logger;
pub mod pm_auth;
#[cfg(feature = "recon")]
pub mod recon;
#[cfg(feature = "email")]
pub mod email;

View File

@ -12,10 +12,14 @@ use serde::Serialize;
use super::authorization::{self, permissions::Permission};
#[cfg(feature = "olap")]
use super::jwt;
#[cfg(feature = "recon")]
use super::recon::ReconToken;
#[cfg(feature = "olap")]
use crate::consts;
#[cfg(feature = "olap")]
use crate::core::errors::UserResult;
#[cfg(feature = "recon")]
use crate::routes::AppState;
use crate::{
configs::settings,
core::{
@ -822,3 +826,95 @@ where
}
default_auth
}
#[cfg(feature = "recon")]
static RECON_API_KEY: tokio::sync::OnceCell<StrongSecret<String>> =
tokio::sync::OnceCell::const_new();
#[cfg(feature = "recon")]
pub async fn get_recon_admin_api_key(
secrets: &settings::Secrets,
#[cfg(feature = "kms")] kms_client: &kms::KmsClient,
) -> RouterResult<&'static StrongSecret<String>> {
RECON_API_KEY
.get_or_try_init(|| async {
#[cfg(feature = "kms")]
let recon_admin_api_key = secrets
.kms_encrypted_recon_admin_api_key
.decrypt_inner(kms_client)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to KMS decrypt recon admin API key")?;
#[cfg(not(feature = "kms"))]
let recon_admin_api_key = secrets.recon_admin_api_key.clone();
Ok(StrongSecret::new(recon_admin_api_key))
})
.await
}
#[cfg(feature = "recon")]
pub struct ReconAdmin;
#[async_trait]
#[cfg(feature = "recon")]
impl<A> AuthenticateAndFetch<(), A> for ReconAdmin
where
A: AppStateInfo + Sync,
{
async fn authenticate_and_fetch(
&self,
request_headers: &HeaderMap,
state: &A,
) -> RouterResult<((), AuthenticationType)> {
let request_admin_api_key =
get_api_key(request_headers).change_context(errors::ApiErrorResponse::Unauthorized)?;
let conf = state.conf();
let admin_api_key = get_recon_admin_api_key(
&conf.secrets,
#[cfg(feature = "kms")]
kms::get_kms_client(&conf.kms).await,
)
.await?;
if request_admin_api_key != admin_api_key.peek() {
Err(report!(errors::ApiErrorResponse::Unauthorized)
.attach_printable("Recon Admin Authentication Failure"))?;
}
Ok(((), AuthenticationType::NoAuth))
}
}
#[cfg(feature = "recon")]
pub struct ReconJWT;
#[cfg(feature = "recon")]
pub struct ReconUser {
pub user_id: String,
}
#[cfg(feature = "recon")]
impl AuthInfo for ReconUser {
fn get_merchant_id(&self) -> Option<&str> {
None
}
}
#[cfg(all(feature = "olap", feature = "recon"))]
#[async_trait]
impl AuthenticateAndFetch<ReconUser, AppState> for ReconJWT {
async fn authenticate_and_fetch(
&self,
request_headers: &HeaderMap,
state: &AppState,
) -> RouterResult<(ReconUser, AuthenticationType)> {
let payload = parse_jwt_payload::<AppState, ReconToken>(request_headers, state).await?;
Ok((
ReconUser {
user_id: payload.user_id,
},
AuthenticationType::NoAuth,
))
}
}

View File

@ -1,17 +1,37 @@
use common_utils::errors::CustomResult;
use error_stack::ResultExt;
use external_services::email::{EmailContents, EmailData, EmailError};
use masking::ExposeInterface;
use masking::{ExposeInterface, PeekInterface};
use crate::{configs, consts};
#[cfg(feature = "olap")]
use crate::{core::errors::UserErrors, services::jwt, types::domain};
pub enum EmailBody {
Verify { link: String },
Reset { link: String, user_name: String },
MagicLink { link: String, user_name: String },
InviteUser { link: String, user_name: String },
Verify {
link: String,
},
Reset {
link: String,
user_name: String,
},
MagicLink {
link: String,
user_name: String,
},
InviteUser {
link: String,
user_name: String,
},
ReconActivation {
user_name: String,
},
ProFeatureRequest {
feature_name: String,
merchant_id: String,
user_name: String,
user_email: String,
},
}
pub mod html {
@ -43,6 +63,30 @@ pub mod html {
link = link
)
}
EmailBody::ReconActivation { user_name } => {
format!(
include_str!("assets/recon_activation.html"),
username = user_name,
)
}
EmailBody::ProFeatureRequest {
feature_name,
merchant_id,
user_name,
user_email,
} => {
format!(
"Dear Hyperswitch Support Team,
Dashboard Pro Feature Request,
Feature name : {feature_name}
Merchant ID : {merchant_id}
Merchant Name : {user_name}
Email : {user_email}
(note: This is an auto generated email. use merchant email for any further comunications)",
)
}
}
}
}
@ -198,3 +242,54 @@ impl EmailData for InviteUser {
})
}
}
pub struct ReconActivation {
pub recipient_email: domain::UserEmail,
pub user_name: domain::UserName,
pub settings: std::sync::Arc<configs::settings::Settings>,
pub subject: &'static str,
}
#[async_trait::async_trait]
impl EmailData for ReconActivation {
async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
let body = html::get_html_body(EmailBody::ReconActivation {
user_name: self.user_name.clone().get_secret().expose(),
});
Ok(EmailContents {
subject: self.subject.to_string(),
body: external_services::email::IntermediateString::new(body),
recipient: self.recipient_email.clone().into_inner(),
})
}
}
pub struct ProFeatureRequest {
pub recipient_email: domain::UserEmail,
pub feature_name: String,
pub merchant_id: String,
pub user_name: domain::UserName,
pub settings: std::sync::Arc<configs::settings::Settings>,
pub subject: String,
}
#[async_trait::async_trait]
impl EmailData for ProFeatureRequest {
async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
let recipient = self.recipient_email.clone().into_inner();
let body = html::get_html_body(EmailBody::ProFeatureRequest {
user_name: self.user_name.clone().get_secret().expose(),
feature_name: self.feature_name.clone(),
merchant_id: self.merchant_id.clone(),
user_email: recipient.peek().to_string(),
});
Ok(EmailContents {
subject: self.subject.clone(),
body: external_services::email::IntermediateString::new(body),
recipient,
})
}
}

View File

@ -0,0 +1,29 @@
use error_stack::ResultExt;
use masking::Secret;
use super::jwt;
use crate::{
consts,
core::{self, errors::RouterResult},
routes::app::settings::Settings,
};
#[derive(serde::Serialize, serde::Deserialize)]
pub struct ReconToken {
pub user_id: String,
pub exp: u64,
}
impl ReconToken {
pub async fn new_token(user_id: String, settings: &Settings) -> RouterResult<Secret<String>> {
let exp_duration = std::time::Duration::from_secs(consts::JWT_TOKEN_TIME_IN_SECS);
let exp = jwt::generate_exp(exp_duration)
.change_context(core::errors::ApiErrorResponse::InternalServerError)?
.as_secs();
let token_payload = Self { user_id, exp };
let token = jwt::generate_jwt(&token_payload, settings)
.await
.change_context(core::errors::ApiErrorResponse::InternalServerError)?;
Ok(Secret::new(token))
}
}

View File

@ -165,6 +165,14 @@ pub enum Flow {
RefundsList,
// Retrieve forex flow.
RetrieveForexFlow,
/// Toggles recon service for a merchant.
ReconMerchantUpdate,
/// Recon token request flow.
ReconTokenRequest,
/// Initial request for recon service.
ReconServiceRequest,
/// Recon token verification flow
ReconVerifyToken,
/// Routing create flow,
RoutingCreateConfig,
/// Routing link config