feat(authentication): added profile acquirer create module (#8155)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Sahkal Poddar
2025-06-11 18:50:19 +05:30
committed by GitHub
parent 5ce2ab2d05
commit f54d785ed4
32 changed files with 699 additions and 22 deletions

View File

@ -61,6 +61,7 @@ pub mod verification;
pub mod verify_connector;
pub mod webhooks;
pub mod profile_acquirer;
pub mod unified_authentication_service;
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]

View File

@ -372,13 +372,13 @@ pub async fn payouts_confirm_core(
merchant_context: domain::MerchantContext,
req: payouts::PayoutCreateRequest,
) -> RouterResponse<payouts::PayoutCreateResponse> {
let mut payout_data = make_payout_data(
let mut payout_data = Box::pin(make_payout_data(
&state,
&merchant_context,
None,
&payouts::PayoutRequest::PayoutCreateRequest(Box::new(req.to_owned())),
&state.locale,
)
))
.await?;
let payout_attempt = payout_data.payout_attempt.to_owned();
let status = payout_attempt.status;
@ -435,13 +435,13 @@ pub async fn payouts_update_core(
req: payouts::PayoutCreateRequest,
) -> RouterResponse<payouts::PayoutCreateResponse> {
let payout_id = req.payout_id.clone().get_required_value("payout_id")?;
let mut payout_data = make_payout_data(
let mut payout_data = Box::pin(make_payout_data(
&state,
&merchant_context,
None,
&payouts::PayoutRequest::PayoutCreateRequest(Box::new(req.to_owned())),
&state.locale,
)
))
.await?;
let payout_attempt = payout_data.payout_attempt.to_owned();
@ -509,13 +509,13 @@ pub async fn payouts_retrieve_core(
profile_id: Option<common_utils::id_type::ProfileId>,
req: payouts::PayoutRetrieveRequest,
) -> RouterResponse<payouts::PayoutCreateResponse> {
let mut payout_data = make_payout_data(
let mut payout_data = Box::pin(make_payout_data(
&state,
&merchant_context,
profile_id,
&payouts::PayoutRequest::PayoutRetrieveRequest(req.to_owned()),
&state.locale,
)
))
.await?;
let payout_attempt = payout_data.payout_attempt.to_owned();
let status = payout_attempt.status;
@ -550,13 +550,13 @@ pub async fn payouts_cancel_core(
merchant_context: domain::MerchantContext,
req: payouts::PayoutActionRequest,
) -> RouterResponse<payouts::PayoutCreateResponse> {
let mut payout_data = make_payout_data(
let mut payout_data = Box::pin(make_payout_data(
&state,
&merchant_context,
None,
&payouts::PayoutRequest::PayoutActionRequest(req.to_owned()),
&state.locale,
)
))
.await?;
let payout_attempt = payout_data.payout_attempt.to_owned();
@ -641,13 +641,13 @@ pub async fn payouts_fulfill_core(
merchant_context: domain::MerchantContext,
req: payouts::PayoutActionRequest,
) -> RouterResponse<payouts::PayoutCreateResponse> {
let mut payout_data = make_payout_data(
let mut payout_data = Box::pin(make_payout_data(
&state,
&merchant_context,
None,
&payouts::PayoutRequest::PayoutActionRequest(req.to_owned()),
&state.locale,
)
))
.await?;
let payout_attempt = payout_data.payout_attempt.to_owned();

View File

@ -0,0 +1,114 @@
use api_models::profile_acquirer;
use common_utils::types::keymanager::KeyManagerState;
use error_stack::ResultExt;
use crate::{
core::errors::{self, utils::StorageErrorExt, RouterResponse},
services::api,
types::domain,
SessionState,
};
#[cfg(all(feature = "olap", feature = "v1"))]
pub async fn create_profile_acquirer(
state: SessionState,
request: profile_acquirer::ProfileAcquirerCreate,
merchant_context: domain::MerchantContext,
) -> RouterResponse<profile_acquirer::ProfileAcquirerResponse> {
let db = state.store.as_ref();
let profile_acquirer_id = common_utils::generate_profile_acquirer_id_of_default_length();
let key_manager_state: KeyManagerState = (&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,
&request.profile_id,
)
.await
.to_not_found_response(errors::ApiErrorResponse::ProfileNotFound {
id: request.profile_id.get_string_repr().to_owned(),
})?;
let incoming_acquirer_config = common_types::domain::AcquirerConfig {
acquirer_assigned_merchant_id: request.acquirer_assigned_merchant_id.clone(),
merchant_name: request.merchant_name.clone(),
merchant_country_code: request.merchant_country_code,
network: request.network.clone(),
acquirer_bin: request.acquirer_bin.clone(),
acquirer_ica: request.acquirer_ica.clone(),
acquirer_fraud_rate: request.acquirer_fraud_rate,
};
// Check for duplicates before proceeding
business_profile
.acquirer_config_map
.as_ref()
.map_or(Ok(()), |configs_wrapper| {
match configs_wrapper.0.values().any(|existing_config| existing_config == &incoming_acquirer_config) {
true => Err(error_stack::report!(
errors::ApiErrorResponse::GenericDuplicateError {
message: format!(
"Duplicate acquirer configuration found for profile_id: {}. Conflicting configuration: {:?}",
request.profile_id.get_string_repr(),
incoming_acquirer_config
),
}
)),
false => Ok(()),
}
})?;
// Get a mutable reference to the HashMap inside AcquirerConfigMap,
// initializing if it's None or the inner HashMap is not present.
let configs_map = &mut business_profile
.acquirer_config_map
.get_or_insert_with(|| {
common_types::domain::AcquirerConfigMap(std::collections::HashMap::new())
})
.0;
configs_map.insert(
profile_acquirer_id.clone(),
incoming_acquirer_config.clone(),
);
let profile_update = domain::ProfileUpdate::AcquirerConfigMapUpdate {
acquirer_config_map: business_profile.acquirer_config_map.clone(),
};
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 new acquirer config")?;
let updated_acquire_details = updated_business_profile
.acquirer_config_map
.as_ref()
.and_then(|acquirer_configs_wrapper| acquirer_configs_wrapper.0.get(&profile_acquirer_id))
.ok_or(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to get updated acquirer config")?;
let response = profile_acquirer::ProfileAcquirerResponse {
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,
};
Ok(api::ApplicationResponse::Json(response))
}

View File

@ -822,13 +822,13 @@ async fn payouts_incoming_webhook_flow(
payout_id: payouts.payout_id.clone(),
});
let payout_data = payouts::make_payout_data(
let payout_data = Box::pin(payouts::make_payout_data(
&state,
&merchant_context,
None,
&action_req,
common_utils::consts::DEFAULT_LOCALE,
)
))
.await?;
let updated_payout_attempt = db

View File

@ -10,8 +10,7 @@ use common_utils::{
#[cfg(feature = "v2")]
use diesel_models::ephemeral_key::{ClientSecretType, ClientSecretTypeNew};
use diesel_models::{
enums,
enums::ProcessTrackerStatus,
enums::{self, ProcessTrackerStatus},
ephemeral_key::{EphemeralKey, EphemeralKeyNew},
reverse_lookup::{ReverseLookup, ReverseLookupNew},
user_role as user_storage,

View File

@ -132,7 +132,8 @@ pub fn mk_app(
{
server_app = server_app
.service(routes::ProfileNew::server(state.clone()))
.service(routes::Forex::server(state.clone()));
.service(routes::Forex::server(state.clone()))
.service(routes::ProfileAcquirer::server(state.clone()));
}
server_app = server_app.service(routes::Profile::server(state.clone()));

View File

@ -41,6 +41,8 @@ pub mod payouts;
pub mod pm_auth;
pub mod poll;
#[cfg(feature = "olap")]
pub mod profile_acquirer;
#[cfg(feature = "olap")]
pub mod profiles;
#[cfg(feature = "recon")]
pub mod recon;
@ -86,8 +88,8 @@ pub use self::app::{
ApiKeys, AppState, ApplePayCertificatesMigration, Cache, Cards, Configs, ConnectorOnboarding,
Customers, Disputes, EphemeralKey, FeatureMatrix, Files, Forex, Gsm, Health, Hypersense,
Mandates, MerchantAccount, MerchantConnectorAccount, PaymentLink, PaymentMethods, Payments,
Poll, ProcessTracker, Profile, ProfileNew, Refunds, Relay, RelayWebhooks, SessionState,
ThreeDsDecisionRule, User, Webhooks,
Poll, ProcessTracker, Profile, ProfileAcquirer, ProfileNew, Refunds, Relay, RelayWebhooks,
SessionState, ThreeDsDecisionRule, User, Webhooks,
};
#[cfg(feature = "olap")]
pub use self::app::{Blocklist, Organization, Routing, Verify, WebhookEvents};

View File

@ -85,6 +85,8 @@ use crate::routes::cards_info::{
use crate::routes::feature_matrix;
#[cfg(all(feature = "frm", feature = "oltp"))]
use crate::routes::fraud_check as frm_routes;
#[cfg(all(feature = "olap", feature = "v1"))]
use crate::routes::profile_acquirer;
#[cfg(all(feature = "recon", feature = "olap"))]
use crate::routes::recon as recon_routes;
pub use crate::{
@ -2645,3 +2647,17 @@ impl ProcessTracker {
)
}
}
#[cfg(feature = "olap")]
pub struct ProfileAcquirer;
#[cfg(all(feature = "olap", feature = "v1"))]
impl ProfileAcquirer {
pub fn server(state: AppState) -> Scope {
web::scope("/profile_acquirer")
.app_data(web::Data::new(state))
.service(
web::resource("").route(web::post().to(profile_acquirer::create_profile_acquirer)),
)
}
}

View File

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

View File

@ -0,0 +1,50 @@
use actix_web::{web, HttpRequest, HttpResponse};
use api_models::profile_acquirer::ProfileAcquirerCreate;
use router_env::{instrument, tracing, Flow};
use super::app::AppState;
use crate::{
core::api_locking,
services::{api, authentication as auth, authorization::permissions::Permission},
types::domain,
};
#[cfg(all(feature = "olap", feature = "v1"))]
#[instrument(skip_all, fields(flow = ?Flow::ProfileAcquirerCreate))]
pub async fn create_profile_acquirer(
state: web::Data<AppState>,
req: HttpRequest,
json_payload: web::Json<ProfileAcquirerCreate>,
) -> HttpResponse {
let flow = Flow::ProfileAcquirerCreate;
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::create_profile_acquirer(
state,
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

@ -197,6 +197,7 @@ 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,
is_iframe_redirection_enabled: item.is_iframe_redirection_enabled,
})
}

View File

@ -53,9 +53,14 @@ impl ProcessTrackerWorkflow<SessionState> for AttachPayoutAccountWorkflow {
merchant_account.clone(),
key_store.clone(),
)));
let mut payout_data =
payouts::make_payout_data(state, &merchant_context, None, &request, DEFAULT_LOCALE)
.await?;
let mut payout_data = Box::pin(payouts::make_payout_data(
state,
&merchant_context,
None,
&request,
DEFAULT_LOCALE,
))
.await?;
payouts::payouts_core(state, &merchant_context, &mut payout_data, None, None).await?;

View File

@ -542,13 +542,13 @@ async fn get_outgoing_webhook_content_and_event_type(
payout_models::PayoutActionRequest { payout_id },
);
let payout_data = payouts::make_payout_data(
let payout_data = Box::pin(payouts::make_payout_data(
&state,
&merchant_context,
None,
&request,
DEFAULT_LOCALE,
)
))
.await?;
let router_response =