feat(authentication): create api for update profile acquirer (#8307)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Sahkal Poddar
2025-06-12 17:39:15 +05:30
committed by GitHub
parent 261818f215
commit d33e344f82
14 changed files with 417 additions and 24 deletions

View File

@ -285,6 +285,8 @@ pub enum StripeErrorCode {
PlatformBadRequest,
#[error(error_type = StripeErrorType::HyperswitchError, code = "", message = "Platform Unauthorized Request")]
PlatformUnauthorizedRequest,
#[error(error_type = StripeErrorType::HyperswitchError, code = "", message = "Profile Acquirer not found")]
ProfileAcquirerNotFound,
// [#216]: https://github.com/juspay/hyperswitch/issues/216
// Implement the remaining stripe error codes
@ -688,6 +690,9 @@ impl From<errors::ApiErrorResponse> for StripeErrorCode {
}
errors::ApiErrorResponse::PlatformAccountAuthNotSupported => Self::PlatformBadRequest,
errors::ApiErrorResponse::InvalidPlatformOperation => Self::PlatformUnauthorizedRequest,
errors::ApiErrorResponse::ProfileAcquirerNotFound { .. } => {
Self::ProfileAcquirerNotFound
}
}
}
}
@ -782,6 +787,7 @@ impl actix_web::ResponseError for StripeErrorCode {
StatusCode::from_u16(*code).unwrap_or(StatusCode::OK)
}
Self::LockTimeout => StatusCode::LOCKED,
Self::ProfileAcquirerNotFound => StatusCode::NOT_FOUND,
}
}

View File

@ -96,19 +96,127 @@ pub async fn create_profile_acquirer(
.ok_or(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to get updated acquirer config")?;
let response = profile_acquirer::ProfileAcquirerResponse {
let response = profile_acquirer::ProfileAcquirerResponse::from((
profile_acquirer_id,
profile_id: request.profile_id.clone(),
acquirer_assigned_merchant_id: updated_acquire_details
.acquirer_assigned_merchant_id
.clone(),
merchant_name: updated_acquire_details.merchant_name.clone(),
merchant_country_code: updated_acquire_details.merchant_country_code,
network: updated_acquire_details.network.clone(),
acquirer_bin: updated_acquire_details.acquirer_bin.clone(),
acquirer_ica: updated_acquire_details.acquirer_ica.clone(),
acquirer_fraud_rate: updated_acquire_details.acquirer_fraud_rate,
};
&request.profile_id,
updated_acquire_details,
));
Ok(api::ApplicationResponse::Json(response))
}
#[cfg(all(feature = "olap", feature = "v1"))]
pub async fn update_profile_acquirer_config(
state: SessionState,
profile_id: common_utils::id_type::ProfileId,
profile_acquirer_id: common_utils::id_type::ProfileAcquirerId,
request: profile_acquirer::ProfileAcquirerUpdate,
merchant_context: domain::MerchantContext,
) -> RouterResponse<profile_acquirer::ProfileAcquirerResponse> {
let db = state.store.as_ref();
let key_manager_state = (&state).into();
let merchant_key_store = merchant_context.get_merchant_key_store();
let mut business_profile = db
.find_business_profile_by_profile_id(&key_manager_state, merchant_key_store, &profile_id)
.await
.to_not_found_response(errors::ApiErrorResponse::ProfileNotFound {
id: profile_id.get_string_repr().to_owned(),
})?;
let acquirer_config_map = business_profile
.acquirer_config_map
.as_mut()
.ok_or(errors::ApiErrorResponse::ProfileAcquirerNotFound {
profile_id: profile_id.get_string_repr().to_owned(),
profile_acquirer_id: profile_acquirer_id.get_string_repr().to_owned(),
})
.attach_printable("no acquirer config found in business profile")?;
let mut potential_updated_config = acquirer_config_map
.0
.get(&profile_acquirer_id)
.ok_or_else(|| errors::ApiErrorResponse::ProfileAcquirerNotFound {
profile_id: profile_id.get_string_repr().to_owned(),
profile_acquirer_id: profile_acquirer_id.get_string_repr().to_owned(),
})?
.clone();
// updating value in existing acquirer config
request
.acquirer_assigned_merchant_id
.map(|val| potential_updated_config.acquirer_assigned_merchant_id = val);
request
.merchant_name
.map(|val| potential_updated_config.merchant_name = val);
request
.merchant_country_code
.map(|val| potential_updated_config.merchant_country_code = val);
request
.network
.map(|val| potential_updated_config.network = val);
request
.acquirer_bin
.map(|val| potential_updated_config.acquirer_bin = val);
request
.acquirer_ica
.map(|val| potential_updated_config.acquirer_ica = Some(val.clone()));
request
.acquirer_fraud_rate
.map(|val| potential_updated_config.acquirer_fraud_rate = val);
// checking for duplicates in the acquirerConfigMap
match acquirer_config_map
.0
.iter()
.find(|(_existing_id, existing_config_val_ref)| {
**existing_config_val_ref == potential_updated_config
}) {
Some((conflicting_id_of_found_item, _)) => {
Err(error_stack::report!(errors::ApiErrorResponse::GenericDuplicateError {
message: format!(
"Duplicate acquirer configuration. This configuration already exists for profile_acquirer_id '{}' under profile_id '{}'.",
conflicting_id_of_found_item.get_string_repr(),
profile_id.get_string_repr()
),
}))
}
None => Ok(()),
}?;
acquirer_config_map
.0
.insert(profile_acquirer_id.clone(), potential_updated_config);
let updated_map_for_db_update = business_profile.acquirer_config_map.clone();
let profile_update = domain::ProfileUpdate::AcquirerConfigMapUpdate {
acquirer_config_map: updated_map_for_db_update,
};
let updated_business_profile = db
.update_profile_by_profile_id(
&key_manager_state,
merchant_key_store,
business_profile,
profile_update,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to update business profile with updated acquirer config")?;
let final_acquirer_details = updated_business_profile
.acquirer_config_map
.as_ref()
.and_then(|configs_wrapper| configs_wrapper.0.get(&profile_acquirer_id))
.ok_or(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to get updated acquirer config after DB update")?;
let response = profile_acquirer::ProfileAcquirerResponse::from((
profile_acquirer_id,
&profile_id,
final_acquirer_details,
));
Ok(api::ApplicationResponse::Json(response))
}

View File

@ -2634,5 +2634,9 @@ impl ProfileAcquirer {
.service(
web::resource("").route(web::post().to(profile_acquirer::create_profile_acquirer)),
)
.service(
web::resource("/{profile_id}/{profile_acquirer_id}")
.route(web::post().to(profile_acquirer::profile_acquirer_update)),
)
}
}

View File

@ -346,7 +346,7 @@ impl From<Flow> for ApiIdentifier {
Flow::RevenueRecoveryRetrieve => Self::ProcessTracker,
Flow::Proxy => Self::Proxy,
Flow::ProfileAcquirerCreate => Self::ProfileAcquirer,
Flow::ProfileAcquirerCreate | Flow::ProfileAcquirerUpdate => Self::ProfileAcquirer,
Flow::ThreeDsDecisionRuleExecute => Self::ThreeDsDecisionRule,
Flow::TokenizationCreate | Flow::TokenizationRetrieve => Self::GenericTokenization,
}

View File

@ -1,5 +1,5 @@
use actix_web::{web, HttpRequest, HttpResponse};
use api_models::profile_acquirer::ProfileAcquirerCreate;
use api_models::profile_acquirer::{ProfileAcquirerCreate, ProfileAcquirerUpdate};
use router_env::{instrument, tracing, Flow};
use super::app::AppState;
@ -48,3 +48,50 @@ pub async fn create_profile_acquirer(
))
.await
}
#[cfg(all(feature = "olap", feature = "v1"))]
#[instrument(skip_all, fields(flow = ?Flow::ProfileAcquirerUpdate))]
pub async fn profile_acquirer_update(
state: web::Data<AppState>,
req: HttpRequest,
path: web::Path<(
common_utils::id_type::ProfileId,
common_utils::id_type::ProfileAcquirerId,
)>,
json_payload: web::Json<ProfileAcquirerUpdate>,
) -> HttpResponse {
let flow = Flow::ProfileAcquirerUpdate;
let (profile_id, profile_acquirer_id) = path.into_inner();
let payload = json_payload.into_inner();
Box::pin(api::server_wrap(
flow,
state,
&req,
payload,
|state: super::SessionState, auth_data, req, _| {
let merchant_context = domain::MerchantContext::NormalMerchant(Box::new(
domain::Context(auth_data.merchant_account, auth_data.key_store),
));
crate::core::profile_acquirer::update_profile_acquirer_config(
state,
profile_id.clone(),
profile_acquirer_id.clone(),
req,
merchant_context.clone(),
)
},
auth::auth_type(
&auth::HeaderAuth(auth::ApiKeyAuth {
is_connected_allowed: false,
is_platform_allowed: true,
}),
&auth::JWTAuth {
permission: Permission::ProfileAccountWrite,
},
req.headers(),
),
api_locking::LockAction::NotApplicable,
))
.await
}

View File

@ -36,6 +36,32 @@ use crate::{
utils,
};
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct ProfileAcquirerConfigs {
pub acquirer_config_map: Option<common_types::domain::AcquirerConfigMap>,
pub profile_id: common_utils::id_type::ProfileId,
}
impl From<ProfileAcquirerConfigs>
for Option<Vec<api_models::profile_acquirer::ProfileAcquirerResponse>>
{
fn from(item: ProfileAcquirerConfigs) -> Self {
item.acquirer_config_map.map(|config_map_val| {
let mut vec: Vec<_> = config_map_val.0.into_iter().collect();
vec.sort_by_key(|k| k.0.clone());
vec.into_iter()
.map(|(profile_acquirer_id, acquirer_config)| {
api_models::profile_acquirer::ProfileAcquirerResponse::from((
profile_acquirer_id,
&item.profile_id,
&acquirer_config,
))
})
.collect::<Vec<api_models::profile_acquirer::ProfileAcquirerResponse>>()
})
}
}
impl ForeignFrom<diesel_models::organization::Organization> for OrganizationResponse {
fn foreign_from(org: diesel_models::organization::Organization) -> Self {
Self {
@ -148,7 +174,7 @@ impl ForeignTryFrom<domain::Profile> for ProfileResponse {
Ok(Self {
merchant_id: item.merchant_id,
profile_id,
profile_id: profile_id.clone(),
profile_name: item.profile_name,
return_url: item.return_url,
enable_payment_response_hash: item.enable_payment_response_hash,
@ -197,7 +223,11 @@ impl ForeignTryFrom<domain::Profile> for ProfileResponse {
is_debit_routing_enabled: Some(item.is_debit_routing_enabled),
merchant_business_country: item.merchant_business_country,
is_pre_network_tokenization_enabled: item.is_pre_network_tokenization_enabled,
acquirer_configs: item.acquirer_config_map,
acquirer_configs: ProfileAcquirerConfigs {
acquirer_config_map: item.acquirer_config_map.clone(),
profile_id: profile_id.clone(),
}
.into(),
is_iframe_redirection_enabled: item.is_iframe_redirection_enabled,
merchant_category_code: item.merchant_category_code,
})