Files

643 lines
22 KiB
Rust

use api_models::admin::PrimaryBusinessDetails;
use common_utils::ext_traits::ValueExt;
use error_stack::{report, FutureExt, IntoReport, ResultExt};
use storage_models::{enums, merchant_account};
use uuid::Uuid;
use crate::{
consts,
core::{
api_keys,
errors::{self, RouterResponse, RouterResult, StorageErrorExt},
payments::helpers,
},
db::StorageInterface,
routes::AppState,
services::api as service_api,
types::{
self, api,
storage::{self, MerchantAccount},
transformers::{ForeignInto, ForeignTryFrom, ForeignTryInto},
},
utils::{self, OptionExt},
};
#[inline]
pub fn create_merchant_publishable_key() -> String {
format!(
"pk_{}_{}",
router_env::env::prefix_for_env(),
Uuid::new_v4().simple()
)
}
fn get_primary_business_details(
request: &api::MerchantAccountCreate,
) -> Vec<PrimaryBusinessDetails> {
// In this case, business details is not optional, it will always be passed
#[cfg(feature = "multiple_mca")]
{
request.primary_business_details.to_owned()
}
// In this case, business details will be optional, if it is not passed, then create the
// default value
#[cfg(not(feature = "multiple_mca"))]
{
request
.primary_business_details
.to_owned()
.unwrap_or_else(|| {
vec![PrimaryBusinessDetails {
country: enums::CountryCode::US,
business: "default".to_string(),
}]
})
}
}
pub async fn create_merchant_account(
state: &AppState,
req: api::MerchantAccountCreate,
) -> RouterResponse<api::MerchantAccountResponse> {
let db = &*state.store;
let publishable_key = Some(create_merchant_publishable_key());
let api_key_request = api::CreateApiKeyRequest {
name: "Default API key".into(),
description: Some(
"An API key created by default when a user signs up on the HyperSwitch dashboard"
.into(),
),
expiration: api::ApiKeyExpiration::Never,
};
let api_key = match api_keys::create_api_key(
db,
&state.conf.api_keys,
#[cfg(feature = "kms")]
&state.conf.kms,
api_key_request,
req.merchant_id.clone(),
)
.await?
{
service_api::ApplicationResponse::Json(api::CreateApiKeyResponse { api_key, .. }) => {
Ok(api_key)
}
_ => Err(errors::ApiErrorResponse::InternalServerError)
.into_report()
.attach_printable("Unexpected create API key response"),
}?;
let primary_business_details =
utils::Encode::<api::WebhookDetails>::encode_to_value(&get_primary_business_details(&req))
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "primary_business_details",
})?;
let merchant_details =
req.merchant_details
.as_ref()
.map(|merchant_details| {
utils::Encode::<api::MerchantDetails>::encode_to_value(merchant_details)
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "merchant_details",
})
})
.transpose()?;
let webhook_details =
req.webhook_details
.as_ref()
.map(|webhook_details| {
utils::Encode::<api::WebhookDetails>::encode_to_value(webhook_details)
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "webhook details",
})
})
.transpose()?;
if let Some(ref routing_algorithm) = req.routing_algorithm {
let _: api::RoutingAlgorithm = routing_algorithm
.clone()
.parse_value("RoutingAlgorithm")
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "routing_algorithm",
})
.attach_printable("Invalid routing algorithm given")?;
}
let merchant_account = storage::MerchantAccountNew {
merchant_id: req.merchant_id,
merchant_name: req.merchant_name,
api_key: Some(api_key),
merchant_details,
return_url: req.return_url.map(|a| a.to_string()),
webhook_details,
routing_algorithm: req.routing_algorithm,
sub_merchants_enabled: req.sub_merchants_enabled,
parent_merchant_id: get_parent_merchant(
db,
req.sub_merchants_enabled,
req.parent_merchant_id,
)
.await?,
enable_payment_response_hash: req.enable_payment_response_hash,
payment_response_hash_key: req.payment_response_hash_key,
redirect_to_merchant_with_http_post: req.redirect_to_merchant_with_http_post,
publishable_key,
locker_id: req.locker_id,
metadata: req.metadata,
primary_business_details,
};
let merchant_account = db
.insert_merchant(merchant_account)
.await
.map_err(|error| {
error.to_duplicate_response(errors::ApiErrorResponse::DuplicateMerchantAccount)
})?;
Ok(service_api::ApplicationResponse::Json(
ForeignTryFrom::foreign_try_from(merchant_account).change_context(
errors::ApiErrorResponse::InvalidDataValue {
field_name: "merchant_account",
},
)?,
))
}
pub async fn get_merchant_account(
db: &dyn StorageInterface,
req: api::MerchantId,
) -> RouterResponse<api::MerchantAccountResponse> {
let merchant_account = db
.find_merchant_account_by_merchant_id(&req.merchant_id)
.await
.map_err(|error| {
error.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)
})?;
Ok(service_api::ApplicationResponse::Json(
ForeignTryFrom::foreign_try_from(merchant_account).change_context(
errors::ApiErrorResponse::InvalidDataValue {
field_name: "merchant_account",
},
)?,
))
}
pub async fn merchant_account_update(
db: &dyn StorageInterface,
merchant_id: &String,
req: api::MerchantAccountUpdate,
) -> RouterResponse<api::MerchantAccountResponse> {
if &req.merchant_id != merchant_id {
Err(report!(errors::ValidationError::IncorrectValueProvided {
field_name: "parent_merchant_id"
})
.attach_printable(
"If `sub_merchants_enabled` is true, then `parent_merchant_id` is mandatory",
)
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "parent_merchant_id",
}))?;
}
if let Some(ref routing_algorithm) = req.routing_algorithm {
let _: api::RoutingAlgorithm = routing_algorithm
.clone()
.parse_value("RoutingAlgorithm")
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "routing_algorithm",
})
.attach_printable("Invalid routing algorithm given")?;
}
let updated_merchant_account = storage::MerchantAccountUpdate::Update {
merchant_name: req.merchant_name,
merchant_details: req
.merchant_details
.as_ref()
.map(utils::Encode::<api::MerchantDetails>::encode_to_value)
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)?,
return_url: req.return_url.map(|a| a.to_string()),
webhook_details: req
.webhook_details
.as_ref()
.map(utils::Encode::<api::WebhookDetails>::encode_to_value)
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)?,
routing_algorithm: req.routing_algorithm,
sub_merchants_enabled: req.sub_merchants_enabled,
parent_merchant_id: get_parent_merchant(
db,
req.sub_merchants_enabled,
req.parent_merchant_id,
)
.await?,
enable_payment_response_hash: req.enable_payment_response_hash,
payment_response_hash_key: req.payment_response_hash_key,
redirect_to_merchant_with_http_post: req.redirect_to_merchant_with_http_post,
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
.update_specific_fields_in_merchant(merchant_id, updated_merchant_account)
.await
.map_err(|error| {
error.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)
})?;
Ok(service_api::ApplicationResponse::Json(
ForeignTryFrom::foreign_try_from(response).change_context(
errors::ApiErrorResponse::InvalidDataValue {
field_name: "merchant_account",
},
)?,
))
}
pub async fn merchant_account_delete(
db: &dyn StorageInterface,
merchant_id: String,
) -> RouterResponse<api::MerchantAccountDeleteResponse> {
let is_deleted = db
.delete_merchant_account_by_merchant_id(&merchant_id)
.await
.map_err(|error| {
error.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)
})?;
let response = api::MerchantAccountDeleteResponse {
merchant_id,
deleted: is_deleted,
};
Ok(service_api::ApplicationResponse::Json(response))
}
async fn get_parent_merchant(
db: &dyn StorageInterface,
sub_merchants_enabled: Option<bool>,
parent_merchant: Option<String>,
) -> RouterResult<Option<String>> {
Ok(match sub_merchants_enabled {
Some(true) => {
Some(
parent_merchant.ok_or_else(|| {
report!(errors::ValidationError::MissingRequiredField {
field_name: "parent_merchant_id".to_string()
})
.change_context(errors::ApiErrorResponse::PreconditionFailed {
message: "If `sub_merchants_enabled` is `true`, then `parent_merchant_id` is mandatory".to_string(),
})
})
.map(|id| validate_merchant_id(db, id).change_context(
errors::ApiErrorResponse::InvalidDataValue { field_name: "parent_merchant_id" }
))?
.await?
.merchant_id
)
}
_ => None,
})
}
async fn validate_merchant_id<S: Into<String>>(
db: &dyn StorageInterface,
merchant_id: S,
) -> RouterResult<MerchantAccount> {
db.find_merchant_account_by_merchant_id(&merchant_id.into())
.await
.map_err(|error| {
error.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)
})
}
fn get_business_details_wrapper(
request: &api::MerchantConnectorCreate,
_merchant_account: &MerchantAccount,
) -> RouterResult<(enums::CountryCode, String)> {
#[cfg(feature = "multiple_mca")]
{
// The fields are mandatory
Ok((request.business_country, request.business_label.to_owned()))
}
#[cfg(not(feature = "multiple_mca"))]
{
// If the value is not passed, then take it from Merchant account
helpers::get_business_details(
request.business_country,
request.business_label.as_ref(),
_merchant_account,
)
}
}
pub async fn create_payment_connector(
store: &dyn StorageInterface,
req: api::MerchantConnectorCreate,
merchant_id: &String,
) -> RouterResponse<api_models::admin::MerchantConnectorResponse> {
let merchant_account = store
.find_merchant_account_by_merchant_id(merchant_id)
.await
.map_err(|error| {
error.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)
})?;
let (business_country, business_label) = get_business_details_wrapper(&req, &merchant_account)?;
let connector_label = helpers::get_connector_label(
business_country,
&business_label,
req.business_sub_label.as_ref(),
&req.connector_name,
);
let mut vec = Vec::new();
let payment_methods_enabled = match req.payment_methods_enabled {
Some(val) => {
for pm in val.into_iter() {
let pm_value = utils::Encode::<api::PaymentMethodsEnabled>::encode_to_value(&pm)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable(
"Failed while encoding to serde_json::Value, PaymentMethod",
)?;
vec.push(pm_value)
}
Some(vec)
}
None => None,
};
// Validate Merchant api details and return error if not in correct format
let _: types::ConnectorAuthType = req
.connector_account_details
.clone()
.parse_value("ConnectorAuthType")
.change_context(errors::ApiErrorResponse::InvalidDataFormat {
field_name: "connector_account_details".to_string(),
expected_format: "auth_type and api_key".to_string(),
})?;
let merchant_connector_account = storage::MerchantConnectorAccountNew {
merchant_id: Some(merchant_id.to_string()),
connector_type: Some(req.connector_type.foreign_into()),
connector_name: Some(req.connector_name),
merchant_connector_id: utils::generate_id(consts::ID_LENGTH, "mca"),
connector_account_details: req.connector_account_details,
payment_methods_enabled,
test_mode: req.test_mode,
disabled: req.disabled,
metadata: req.metadata,
connector_label: connector_label.clone(),
business_country,
business_label,
business_sub_label: req.business_sub_label,
};
let mca = store
.insert_merchant_connector_account(merchant_connector_account)
.await
.map_err(|error| {
error.to_duplicate_response(errors::ApiErrorResponse::DuplicateMerchantConnectorAccount)
})?;
let mca_response = ForeignTryFrom::foreign_try_from(mca)?;
Ok(service_api::ApplicationResponse::Json(mca_response))
}
pub async fn retrieve_payment_connector(
store: &dyn StorageInterface,
merchant_id: String,
merchant_connector_id: String,
) -> RouterResponse<api_models::admin::MerchantConnectorResponse> {
let _merchant_account = store
.find_merchant_account_by_merchant_id(&merchant_id)
.await
.map_err(|error| {
error.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)
})?;
let mca = store
.find_by_merchant_connector_account_merchant_id_merchant_connector_id(
&merchant_id,
&merchant_connector_id,
)
.await
.map_err(|error| {
error.to_not_found_response(errors::ApiErrorResponse::MerchantConnectorAccountNotFound)
})?;
Ok(service_api::ApplicationResponse::Json(
ForeignTryFrom::foreign_try_from(mca)?,
))
}
pub async fn list_payment_connectors(
store: &dyn StorageInterface,
merchant_id: String,
) -> RouterResponse<Vec<api_models::admin::MerchantConnectorResponse>> {
// Validate merchant account
store
.find_merchant_account_by_merchant_id(&merchant_id)
.await
.map_err(|err| {
err.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)
})?;
let merchant_connector_accounts = store
.find_merchant_connector_account_by_merchant_id_and_disabled_list(&merchant_id, true)
.await
.map_err(|error| {
error.to_not_found_response(errors::ApiErrorResponse::MerchantConnectorAccountNotFound)
})?;
let mut response = vec![];
// The can be eliminated once [#79711](https://github.com/rust-lang/rust/issues/79711) is stabilized
for mca in merchant_connector_accounts.into_iter() {
response.push(mca.foreign_try_into()?);
}
Ok(service_api::ApplicationResponse::Json(response))
}
pub async fn update_payment_connector(
db: &dyn StorageInterface,
merchant_id: &str,
merchant_connector_id: &str,
req: api_models::admin::MerchantConnectorUpdate,
) -> RouterResponse<api_models::admin::MerchantConnectorResponse> {
let _merchant_account = db
.find_merchant_account_by_merchant_id(merchant_id)
.await
.map_err(|error| {
error.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)
})?;
let mca = db
.find_by_merchant_connector_account_merchant_id_merchant_connector_id(
merchant_id,
merchant_connector_id,
)
.await
.map_err(|error| {
error.to_not_found_response(errors::ApiErrorResponse::MerchantConnectorAccountNotFound)
})?;
let payment_methods_enabled = req.payment_methods_enabled.map(|pm_enabled| {
pm_enabled
.iter()
.flat_map(|payment_method| {
utils::Encode::<api::PaymentMethodsEnabled>::encode_to_value(payment_method)
})
.collect::<Vec<serde_json::Value>>()
});
let payment_connector = storage::MerchantConnectorAccountUpdate::Update {
merchant_id: Some(merchant_id.to_string()),
connector_type: Some(req.connector_type.foreign_into()),
merchant_connector_id: Some(merchant_connector_id.to_string()),
connector_account_details: req.connector_account_details,
payment_methods_enabled,
test_mode: req.test_mode,
disabled: req.disabled,
metadata: req.metadata,
};
let updated_mca = db
.update_merchant_connector_account(mca, payment_connector)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable_lazy(|| {
format!("Failed while updating MerchantConnectorAccount: id: {merchant_connector_id}")
})?;
let mca_response = ForeignTryFrom::foreign_try_from(updated_mca)?;
Ok(service_api::ApplicationResponse::Json(mca_response))
}
pub async fn delete_payment_connector(
db: &dyn StorageInterface,
merchant_id: String,
merchant_connector_id: String,
) -> RouterResponse<api::MerchantConnectorDeleteResponse> {
let _merchant_account = db
.find_merchant_account_by_merchant_id(&merchant_id)
.await
.map_err(|error| {
error.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)
})?;
let is_deleted = db
.delete_merchant_connector_account_by_merchant_id_merchant_connector_id(
&merchant_id,
&merchant_connector_id,
)
.await
.map_err(|error| {
error.to_not_found_response(errors::ApiErrorResponse::MerchantConnectorAccountNotFound)
})?;
let response = api::MerchantConnectorDeleteResponse {
merchant_id,
merchant_connector_id,
deleted: is_deleted,
};
Ok(service_api::ApplicationResponse::Json(response))
}
pub async fn kv_for_merchant(
db: &dyn StorageInterface,
merchant_id: String,
enable: bool,
) -> RouterResponse<api_models::admin::ToggleKVResponse> {
// check if the merchant account exists
let merchant_account = db
.find_merchant_account_by_merchant_id(&merchant_id)
.await
.map_err(|error| {
error.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)
})?;
let updated_merchant_account = match (enable, merchant_account.storage_scheme) {
(true, enums::MerchantStorageScheme::RedisKv)
| (false, enums::MerchantStorageScheme::PostgresOnly) => Ok(merchant_account),
(true, enums::MerchantStorageScheme::PostgresOnly) => {
db.update_merchant(
merchant_account,
merchant_account::MerchantAccountUpdate::StorageSchemeUpdate {
storage_scheme: enums::MerchantStorageScheme::RedisKv,
},
)
.await
}
(false, enums::MerchantStorageScheme::RedisKv) => {
db.update_merchant(
merchant_account,
merchant_account::MerchantAccountUpdate::StorageSchemeUpdate {
storage_scheme: enums::MerchantStorageScheme::PostgresOnly,
},
)
.await
}
}
.map_err(|error| {
error
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("failed to switch merchant_storage_scheme")
})?;
let kv_status = matches!(
updated_merchant_account.storage_scheme,
enums::MerchantStorageScheme::RedisKv
);
Ok(service_api::ApplicationResponse::Json(
api_models::admin::ToggleKVResponse {
merchant_id: updated_merchant_account.merchant_id,
kv_enabled: kv_status,
},
))
}
pub async fn check_merchant_account_kv_status(
db: &dyn StorageInterface,
merchant_id: String,
) -> RouterResponse<api_models::admin::ToggleKVResponse> {
// check if the merchant account exists
let merchant_account = db
.find_merchant_account_by_merchant_id(&merchant_id)
.await
.map_err(|error| {
error.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)
})?;
let kv_status = matches!(
merchant_account.storage_scheme,
enums::MerchantStorageScheme::RedisKv
);
Ok(service_api::ApplicationResponse::Json(
api_models::admin::ToggleKVResponse {
merchant_id: merchant_account.merchant_id,
kv_enabled: kv_status,
},
))
}