feat: multiple connector account support for the same country (#816)

Co-authored-by: Manoj Ghorela <118727120+manoj-juspay@users.noreply.github.com>
Co-authored-by: Arun Raj M <jarnura47@gmail.com>
This commit is contained in:
Narayan Bhat
2023-04-11 17:25:29 +05:30
committed by GitHub
parent 29da1dfa50
commit 6188d51579
40 changed files with 631 additions and 52 deletions

View File

@ -1,5 +1,7 @@
use api_models::admin::PrimaryBusinessDetails;
use common_utils::ext_traits::ValueExt;
use error_stack::{report, FutureExt, IntoReport, ResultExt};
use masking::ExposeInterface;
use storage_models::{enums, merchant_account};
use uuid::Uuid;
@ -8,6 +10,7 @@ use crate::{
core::{
api_keys,
errors::{self, RouterResponse, RouterResult, StorageErrorExt},
payments::helpers,
},
db::StorageInterface,
pii::Secret,
@ -63,19 +66,36 @@ pub async fn create_merchant_account(
.attach_printable("Unexpected create API key response"),
}?;
let merchant_details = Some(
utils::Encode::<api::MerchantDetails>::encode_to_value(&req.merchant_details)
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "merchant_details",
})?,
);
let merchant_details = req
.merchant_details
.map(|md| {
utils::Encode::<api::MerchantDetails>::encode_to_value(&md).change_context(
errors::ApiErrorResponse::InvalidDataValue {
field_name: "merchant_details",
},
)
})
.transpose()?;
let webhook_details = Some(
utils::Encode::<api::WebhookDetails>::encode_to_value(&req.webhook_details)
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "webhook details",
})?,
);
let webhook_details = req
.webhook_details
.map(|wd| {
utils::Encode::<api::WebhookDetails>::encode_to_value(&wd).change_context(
errors::ApiErrorResponse::InvalidDataValue {
field_name: "webhook details",
},
)
})
.transpose()?;
let primary_business_details = req.primary_business_details.expose();
let _valid_business_details: PrimaryBusinessDetails = primary_business_details
.clone()
.parse_value("primary_business_details")
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "primary_business_details",
})?;
if let Some(ref routing_algorithm) = req.routing_algorithm {
let _: api::RoutingAlgorithm = routing_algorithm
@ -108,6 +128,7 @@ pub async fn create_merchant_account(
publishable_key,
locker_id: req.locker_id,
metadata: req.metadata,
primary_business_details,
};
let merchant_account = db
@ -199,6 +220,12 @@ pub async fn merchant_account_update(
locker_id: req.locker_id,
metadata: req.metadata,
publishable_key: None,
primary_business_details: req
.primary_business_details
.as_ref()
.map(utils::Encode::<PrimaryBusinessDetails>::encode_to_value)
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)?,
};
let response = db
@ -268,9 +295,6 @@ async fn validate_merchant_id<S: Into<String>>(
})
}
// Merchant Connector API - Every merchant and connector can have an instance of (merchant <> connector)
// with unique merchant_connector_id for Create Operation
pub async fn create_payment_connector(
store: &dyn StorageInterface,
req: api::MerchantConnector,
@ -283,6 +307,13 @@ pub async fn create_payment_connector(
error.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)
})?;
let connector_label = helpers::get_connector_label(
req.business_country,
&req.business_label,
req.business_sub_label.as_ref(),
&req.connector_name,
);
let mut vec = Vec::new();
let mut response = req.clone();
let payment_methods_enabled = match req.payment_methods_enabled {
@ -320,6 +351,10 @@ pub async fn create_payment_connector(
test_mode: req.test_mode,
disabled: req.disabled,
metadata: req.metadata,
connector_label: connector_label.clone(),
business_country: req.business_country,
business_label: req.business_label,
business_sub_label: req.business_sub_label,
};
let mca = store
@ -330,6 +365,10 @@ pub async fn create_payment_connector(
})?;
response.merchant_connector_id = Some(mca.merchant_connector_id);
response.connector_label = connector_label;
response.business_country = mca.business_country;
response.business_label = mca.business_label;
Ok(service_api::ApplicationResponse::Json(response))
}
@ -392,7 +431,7 @@ pub async fn update_payment_connector(
db: &dyn StorageInterface,
merchant_id: &str,
merchant_connector_id: &str,
req: api::MerchantConnector,
req: api_models::admin::MerchantConnectorUpdate,
) -> RouterResponse<api::MerchantConnector> {
let _merchant_account = db
.find_merchant_account_by_merchant_id(merchant_id)
@ -423,7 +462,6 @@ pub async fn update_payment_connector(
let payment_connector = storage::MerchantConnectorAccountUpdate::Update {
merchant_id: Some(merchant_id.to_string()),
connector_type: Some(req.connector_type.foreign_into()),
connector_name: Some(req.connector_name),
merchant_connector_id: Some(merchant_connector_id.to_string()),
connector_account_details: req.connector_account_details,
payment_methods_enabled,
@ -460,6 +498,10 @@ pub async fn update_payment_connector(
disabled: updated_mca.disabled,
payment_methods_enabled: updated_pm_enabled,
metadata: updated_mca.metadata,
connector_label: updated_mca.connector_label,
business_country: updated_mca.business_country,
business_label: updated_mca.business_label,
business_sub_label: updated_mca.business_sub_label,
};
Ok(service_api::ApplicationResponse::Json(response))
}

View File

@ -443,7 +443,7 @@ impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorRespon
Self::InvalidCardIinLength => AER::BadRequest(ApiError::new("HE", 3, "The provided card IIN length is invalid, please provide an IIN with 6 digits", None)),
Self::FlowNotSupported { flow, connector } => {
AER::BadRequest(ApiError::new("IR", 20, format!("{flow} flow not supported"), Some(Extra {connector: Some(connector.to_owned()), ..Default::default()}))) //FIXME: error message
}
},
}
}
}

View File

@ -2,7 +2,7 @@ use std::borrow::Cow;
use base64::Engine;
use common_utils::{
ext_traits::{AsyncExt, ByteSliceExt},
ext_traits::{AsyncExt, ByteSliceExt, ValueExt},
fp_utils,
};
// TODO : Evaluate all the helper functions ()
@ -1247,6 +1247,95 @@ pub(crate) async fn verify_client_secret(
.transpose()
}
fn connector_needs_business_sub_label(connector_name: &str) -> bool {
let connectors_list = [api_models::enums::Connector::Cybersource];
connectors_list
.map(|connector| connector.to_string())
.contains(&connector_name.to_string())
}
/// Create the connector label
/// {connector_name}_{country}_{business_label}
pub fn get_connector_label(
business_country: api_models::enums::CountryCode,
business_label: &str,
business_sub_label: Option<&String>,
connector_name: &str,
) -> String {
let mut connector_label = format!("{connector_name}_{business_country}_{business_label}");
// Business sub label is currently being used only for cybersource
// To ensure backwards compatibality, cybersource mca's created before this change
// will have the business_sub_label value as default.
//
// Even when creating the connector account, if no sub label is provided, default will be used
if connector_needs_business_sub_label(connector_name) {
if let Some(sub_label) = business_sub_label {
connector_label.push_str(&format!("_{sub_label}"));
} else {
connector_label.push_str("_default"); // For backwards compatibality
}
}
connector_label
}
/// Do lazy parsing of primary business details
/// If both country and label are passed, no need to parse business details from merchant_account
/// If any one is missing, get it from merchant_account
/// If there is more than one label or country configured in merchant account, then
/// passing business details for payment is mandatory to avoid ambiguity
pub fn get_business_details(
business_country: Option<api_enums::CountryCode>,
business_label: Option<&String>,
merchant_account: &storage_models::merchant_account::MerchantAccount,
) -> Result<(api_enums::CountryCode, String), error_stack::Report<errors::ApiErrorResponse>> {
let (business_country, business_label) = match business_country.zip(business_label) {
Some((business_country, business_label)) => {
(business_country.to_owned(), business_label.to_owned())
}
None => {
// Parse the primary business details from merchant account
let primary_business_details: api_models::admin::PrimaryBusinessDetails =
merchant_account
.primary_business_details
.clone()
.parse_value("PrimaryBusinessDetails")
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("failed to parse primary business details")?;
if primary_business_details.country.len() == 1
&& primary_business_details.business.len() == 1
{
let primary_business_country = primary_business_details
.country
.first()
.get_required_value("business_country")?
.to_owned();
let primary_business_label = primary_business_details
.business
.first()
.get_required_value("business_label")?
.to_owned();
(
business_country.unwrap_or(primary_business_country),
business_label
.map(ToString::to_string)
.unwrap_or(primary_business_label),
)
} else {
Err(report!(errors::ApiErrorResponse::MissingRequiredField {
field_name: "business_country, business_label"
}))?
}
}
};
Ok((business_country, business_label))
}
#[inline]
pub(crate) fn get_payment_id_from_client_secret(cs: &str) -> String {
cs.split('_').take(2).collect::<Vec<&str>>().join("_")
@ -1320,7 +1409,7 @@ impl MerchantConnectorAccountType {
pub async fn get_merchant_connector_account(
db: &dyn StorageInterface,
merchant_id: &str,
connector_id: &str,
connector_label: &str,
creds_identifier: Option<String>,
) -> RouterResult<MerchantConnectorAccountType> {
match creds_identifier {
@ -1350,7 +1439,10 @@ pub async fn get_merchant_connector_account(
Ok(MerchantConnectorAccountType::CacheVal(cached_mca))
}
None => db
.find_merchant_connector_account_by_merchant_id_connector(merchant_id, connector_id)
.find_merchant_connector_account_by_merchant_id_connector_label(
merchant_id,
connector_label,
)
.await
.map(MerchantConnectorAccountType::DbVal)
.change_context(errors::ApiErrorResponse::MerchantConnectorAccountNotFound),

View File

@ -174,6 +174,11 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
payment_intent.billing_address_id = billing_address.clone().map(|i| i.address_id);
payment_intent.return_url = request.return_url.as_ref().map(|a| a.to_string());
payment_attempt.business_sub_label = request
.business_sub_label
.clone()
.or(payment_attempt.business_sub_label);
let creds_identifier = request
.merchant_connector_details
.as_ref()
@ -339,6 +344,8 @@ impl<F: Clone> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for Paymen
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to encode additional pm data")?;
let business_sub_label = payment_data.payment_attempt.business_sub_label.clone();
payment_data.payment_attempt = db
.update_payment_attempt_with_attempt_id(
payment_data.payment_attempt,
@ -354,6 +361,7 @@ impl<F: Clone> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for Paymen
payment_method_data: additional_pm_data,
payment_method_type,
payment_experience,
business_sub_label,
},
storage_scheme,
)
@ -370,6 +378,8 @@ impl<F: Clone> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for Paymen
let customer_id = customer.map(|c| c.customer_id);
let return_url = payment_data.payment_intent.return_url.clone();
let setup_future_usage = payment_data.payment_intent.setup_future_usage;
let business_label = Some(payment_data.payment_intent.business_label.clone());
let business_country = Some(payment_data.payment_intent.business_country);
payment_data.payment_intent = db
.update_payment_intent(
@ -383,6 +393,8 @@ impl<F: Clone> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for Paymen
shipping_address_id: shipping_address,
billing_address_id: billing_address,
return_url,
business_country,
business_label,
},
storage_scheme,
)

View File

@ -122,7 +122,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
.insert_payment_intent(
Self::make_payment_intent(
&payment_id,
merchant_id,
merchant_account,
money,
request,
shipping_address.clone().map(|x| x.address_id),
@ -476,7 +476,7 @@ impl PaymentCreate {
#[instrument(skip_all)]
fn make_payment_intent(
payment_id: &str,
merchant_id: &str,
merchant_account: &storage::MerchantAccount,
money: (api::Amount, enums::Currency),
request: &api::PaymentsRequest,
shipping_address_id: Option<String>,
@ -496,9 +496,16 @@ impl PaymentCreate {
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Encoding Metadata to value failed")?;
let (business_country, business_label) = helpers::get_business_details(
request.business_country,
request.business_label.as_ref(),
merchant_account,
)?;
Ok(storage::PaymentIntentNew {
payment_id: payment_id.to_string(),
merchant_id: merchant_id.to_string(),
merchant_id: merchant_account.merchant_id.to_string(),
status,
amount: amount.into(),
currency,
@ -515,6 +522,8 @@ impl PaymentCreate {
statement_descriptor_name: request.statement_descriptor_name.clone(),
statement_descriptor_suffix: request.statement_descriptor_suffix.clone(),
metadata: metadata.map(masking::Secret::new),
business_country,
business_label,
active_attempt_id,
..storage::PaymentIntentNew::default()
})

View File

@ -150,6 +150,19 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
payment_intent.billing_address_id = billing_address.clone().map(|x| x.address_id);
payment_intent.return_url = request.return_url.as_ref().map(|a| a.to_string());
payment_intent.business_country = request
.business_country
.unwrap_or(payment_intent.business_country);
payment_intent.business_label = request
.business_label
.clone()
.unwrap_or(payment_intent.business_label);
payment_attempt.business_sub_label = request
.business_sub_label
.clone()
.or(payment_attempt.business_sub_label);
let token = token.or_else(|| payment_attempt.payment_token.clone());
if request.confirm.unwrap_or(false) {
@ -370,6 +383,8 @@ impl<F: Clone> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for Paymen
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to encode additional pm data")?;
let business_sub_label = payment_data.payment_attempt.business_sub_label.clone();
let payment_method_type = payment_data.payment_attempt.payment_method_type.clone();
let payment_experience = payment_data.payment_attempt.payment_experience.clone();
payment_data.payment_attempt = db
@ -385,6 +400,7 @@ impl<F: Clone> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for Paymen
payment_method_data: additional_pm_data,
payment_experience,
payment_method_type,
business_sub_label,
},
storage_scheme,
)
@ -415,6 +431,8 @@ impl<F: Clone> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for Paymen
let return_url = payment_data.payment_intent.return_url.clone();
let setup_future_usage = payment_data.payment_intent.setup_future_usage;
let business_label = Some(payment_data.payment_intent.business_label.clone());
let business_country = Some(payment_data.payment_intent.business_country);
payment_data.payment_intent = db
.update_payment_intent(
@ -428,6 +446,8 @@ impl<F: Clone> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for Paymen
shipping_address_id: shipping_address,
billing_address_id: billing_address,
return_url,
business_country,
business_label,
},
storage_scheme,
)

View File

@ -36,11 +36,18 @@ where
From<<T as TryFrom<PaymentAdditionalData<'a, F>>>::Error>,
{
let (merchant_connector_account, payment_method, router_data);
let connector_label = helpers::get_connector_label(
payment_data.payment_intent.business_country,
&payment_data.payment_intent.business_label,
payment_data.payment_attempt.business_sub_label.as_ref(),
connector_id,
);
let db = &*state.store;
merchant_connector_account = helpers::get_merchant_connector_account(
db,
merchant_account.merchant_id.as_str(),
connector_id,
&connector_label,
payment_data.creds_identifier.to_owned(),
)
.await?;
@ -274,6 +281,15 @@ where
let routed_through = payment_attempt
.get_routed_through_connector()
.change_context(errors::ApiErrorResponse::InternalServerError)?;
let connector_label = routed_through.as_ref().map(|connector_name| {
helpers::get_connector_label(
payment_intent.business_country,
&payment_intent.business_label,
payment_attempt.business_sub_label.as_ref(),
connector_name,
)
});
services::ApplicationResponse::Json(
response
.set_payment_id(Some(payment_attempt.payment_id))
@ -351,6 +367,10 @@ where
.map(ForeignInto::foreign_into),
)
.set_metadata(payment_intent.metadata)
.set_connector_label(connector_label)
.set_business_country(payment_intent.business_country)
.set_business_label(payment_intent.business_label)
.set_business_sub_label(payment_attempt.business_sub_label)
.to_owned(),
)
}

View File

@ -30,10 +30,18 @@ pub async fn construct_refund_router_data<'a, F>(
creds_identifier: Option<String>,
) -> RouterResult<types::RefundsRouterData<F>> {
let db = &*state.store;
let connector_label = helpers::get_connector_label(
payment_intent.business_country,
&payment_intent.business_label,
None,
connector_id,
);
let merchant_connector_account = helpers::get_merchant_connector_account(
db,
merchant_account.merchant_id.as_str(),
connector_id,
&connector_label,
creds_identifier,
)
.await?;

View File

@ -198,6 +198,7 @@ impl MerchantAccountInterface for MockDb {
storage_scheme: enums::MerchantStorageScheme::PostgresOnly,
locker_id: merchant_account.locker_id,
metadata: merchant_account.metadata,
primary_business_details: merchant_account.primary_business_details,
};
accounts.push(account.clone());
Ok(account)

View File

@ -98,7 +98,7 @@ impl ConnectorAccessToken for MockDb {
#[async_trait::async_trait]
pub trait MerchantConnectorAccountInterface {
async fn find_merchant_connector_account_by_merchant_id_connector(
async fn find_merchant_connector_account_by_merchant_id_connector_label(
&self,
merchant_id: &str,
connector: &str,
@ -136,16 +136,16 @@ pub trait MerchantConnectorAccountInterface {
#[async_trait::async_trait]
impl MerchantConnectorAccountInterface for Store {
async fn find_merchant_connector_account_by_merchant_id_connector(
async fn find_merchant_connector_account_by_merchant_id_connector_label(
&self,
merchant_id: &str,
connector: &str,
connector_label: &str,
) -> CustomResult<storage::MerchantConnectorAccount, errors::StorageError> {
let conn = connection::pg_connection_read(self).await?;
storage::MerchantConnectorAccount::find_by_merchant_id_connector(
&conn,
merchant_id,
connector,
connector_label,
)
.await
.map_err(Into::into)
@ -245,7 +245,7 @@ impl MerchantConnectorAccountInterface for Store {
impl MerchantConnectorAccountInterface for MockDb {
// safety: only used for testing
#[allow(clippy::unwrap_used)]
async fn find_merchant_connector_account_by_merchant_id_connector(
async fn find_merchant_connector_account_by_merchant_id_connector_label(
&self,
merchant_id: &str,
connector: &str,
@ -290,6 +290,10 @@ impl MerchantConnectorAccountInterface for MockDb {
connector_type: t
.connector_type
.unwrap_or(crate::types::storage::enums::ConnectorType::FinOperations),
connector_label: t.connector_label,
business_country: t.business_country,
business_label: t.business_label,
business_sub_label: t.business_sub_label,
};
accounts.push(account.clone());
Ok(account)

View File

@ -263,6 +263,7 @@ impl PaymentAttemptInterface for MockDb {
payment_experience: payment_attempt.payment_experience,
payment_method_type: payment_attempt.payment_method_type,
payment_method_data: payment_attempt.payment_method_data,
business_sub_label: payment_attempt.business_sub_label,
};
payment_attempts.push(payment_attempt.clone());
Ok(payment_attempt)
@ -394,6 +395,7 @@ mod storage {
payment_experience: payment_attempt.payment_experience.clone(),
payment_method_type: payment_attempt.payment_method_type.clone(),
payment_method_data: payment_attempt.payment_method_data.clone(),
business_sub_label: payment_attempt.business_sub_label.clone(),
};
let field = format!("pa_{}", created_attempt.attempt_id);

View File

@ -92,6 +92,8 @@ mod storage {
setup_future_usage: new.setup_future_usage,
off_session: new.off_session,
client_secret: new.client_secret.clone(),
business_country: new.business_country,
business_label: new.business_label.clone(),
active_attempt_id: new.active_attempt_id.to_owned(),
};
@ -348,6 +350,8 @@ impl PaymentIntentInterface for MockDb {
setup_future_usage: new.setup_future_usage,
off_session: new.off_session,
client_secret: new.client_secret,
business_country: new.business_country,
business_label: new.business_label,
active_attempt_id: new.active_attempt_id.to_owned(),
};
payment_intents.push(payment_intent.clone());

View File

@ -294,7 +294,7 @@ pub async fn payment_connector_update(
state: web::Data<AppState>,
req: HttpRequest,
path: web::Path<(String, String)>,
json_payload: web::Json<admin::MerchantConnector>,
json_payload: web::Json<api_models::admin::MerchantConnectorUpdate>,
) -> HttpResponse {
let flow = Flow::MerchantConnectorsUpdate;
let (merchant_id, merchant_connector_id) = path.into_inner();

View File

@ -27,6 +27,7 @@ impl ForeignFrom<storage::MerchantAccount> for MerchantAccountResponse {
publishable_key: item.publishable_key,
metadata: item.metadata,
locker_id: item.locker_id,
primary_business_details: item.primary_business_details.into(),
}
}
}

View File

@ -356,6 +356,10 @@ impl ForeignTryFrom<storage::MerchantConnectorAccount> for api_models::admin::Me
disabled: merchant_ca.disabled,
metadata: merchant_ca.metadata,
payment_methods_enabled,
connector_label: merchant_ca.connector_label,
business_country: merchant_ca.business_country,
business_label: merchant_ca.business_label,
business_sub_label: merchant_ca.business_sub_label,
})
}
}