feat(router): add support for stateful straight through routing (#752)

This commit is contained in:
ItsMeShashank
2023-03-21 17:26:37 +05:30
committed by GitHub
parent 5b5557b71d
commit 568bf01a56
27 changed files with 375 additions and 237 deletions

View File

@ -63,9 +63,16 @@ pub struct PaymentsRequest {
#[serde(default, deserialize_with = "amount::deserialize_option")]
pub amount: Option<Amount>,
#[schema(value_type = Option<RoutingAlgorithm>, example = json!({
"type": "single",
"data": "stripe"
}))]
pub routing: Option<serde_json::Value>,
/// This allows the merchant to manually select a connector with which the payment can go through
#[schema(value_type = Option<Connector>, max_length = 255, example = "stripe")]
#[schema(value_type = Option<Vec<Connector>>, max_length = 255, example = json!(["stripe", "adyen"]))]
pub connector: Option<Vec<api_enums::Connector>>,
/// The currency of the payment request can be specified here
#[schema(value_type = Option<Currency>, example = "USD")]
pub currency: Option<api_enums::Currency>,

View File

@ -475,7 +475,7 @@ pub fn to_currency_base_unit(
| storage_models::enums::Currency::OMR => amount_f64 / 1000.00,
_ => amount_f64 / 100.00,
};
Ok(format!("{:.2}", amount))
Ok(format!("{amount:.2}"))
}
pub fn str_to_f32<S>(value: &str, serializer: S) -> Result<S::Ok, S::Error>
@ -496,7 +496,7 @@ pub fn collect_values_by_removing_signature(
serde_json::Value::Null => vec!["null".to_owned()],
serde_json::Value::Bool(b) => vec![b.to_string()],
serde_json::Value::Number(n) => match n.as_f64() {
Some(f) => vec![format!("{:.2}", f)],
Some(f) => vec![format!("{f:.2}")],
None => vec![n.to_string()],
},
serde_json::Value::String(s) => {

View File

@ -670,12 +670,12 @@ pub async fn add_delete_tokenized_data_task(
})
.into_report()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable_lazy(|| format!("unable to convert into value {:?}", lookup_key))?;
.attach_printable_lazy(|| format!("unable to convert into value {lookup_key:?}"))?;
let schedule_time = get_delete_tokenize_schedule_time(db, &pm, 0).await;
let process_tracker_entry = storage::ProcessTrackerNew {
id: format!("{}_{}", runner, lookup_key),
id: format!("{runner}_{lookup_key}"),
name: Some(String::from(runner)),
tag: vec![String::from("BASILISK-V3")],
runner: Some(String::from(runner)),
@ -694,10 +694,7 @@ pub async fn add_delete_tokenized_data_task(
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable_lazy(|| {
format!(
"Failed while inserting task in process_tracker: lookup_key: {}",
lookup_key
)
format!("Failed while inserting task in process_tracker: lookup_key: {lookup_key}")
})?;
Ok(response)
}

View File

@ -22,7 +22,7 @@ use self::{
};
use crate::{
core::{
errors::{self, RouterResponse, RouterResult},
errors::{self, CustomResult, RouterResponse, RouterResult},
payment_methods::vault,
},
db::StorageInterface,
@ -34,7 +34,7 @@ use crate::{
self, api,
storage::{self, enums as storage_enums},
},
utils::OptionExt,
utils::{Encode, OptionExt, ValueExt},
};
#[instrument(skip_all)]
@ -99,28 +99,14 @@ where
payment_data.payment_method_data = payment_method_data;
let connector_details = operation
.to_domain()?
.get_connector(
&merchant_account,
state,
&req,
payment_data.payment_attempt.connector.as_ref(),
)
.await?;
let connector = match should_call_connector(&operation, &payment_data) {
true => Some(
route_connector(
state,
&merchant_account,
&mut payment_data,
connector_details,
)
.await?,
),
false => None,
};
let connector = get_connector_choice(
&operation,
state,
&req,
&merchant_account,
&mut payment_data,
)
.await?;
let (operation, mut payment_data) = operation
.to_update_tracker()?
@ -133,12 +119,12 @@ where
)
.await?;
operation
.to_domain()?
.add_task_to_process_tracker(state, &payment_data.payment_attempt)
.await?;
if let Some(connector_details) = connector {
operation
.to_domain()?
.add_task_to_process_tracker(state, &payment_data.payment_attempt)
.await?;
payment_data = match connector_details {
api::ConnectorCallType::Single(connector) => {
call_connector_service(
@ -153,6 +139,7 @@ where
)
.await?
}
api::ConnectorCallType::Multiple(connectors) => {
call_multiple_connectors_service(
state,
@ -164,34 +151,6 @@ where
)
.await?
}
api::ConnectorCallType::Routing => {
let connector = payment_data
.payment_attempt
.connector
.clone()
.get_required_value("connector")
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("No connector selected for routing")?;
let connector_data = api::ConnectorData::get_connector_by_name(
&state.conf.connectors,
&connector,
api::GetToken::Connector,
)
.change_context(errors::ApiErrorResponse::InternalServerError)?;
call_connector_service(
state,
&merchant_account,
&validate_result.payment_id,
connector_data,
&operation,
payment_data,
&customer,
call_connector_action,
)
.await?
}
};
if payment_data.payment_intent.status != storage_enums::IntentStatus::RequiresCustomerAction
{
@ -694,7 +653,7 @@ pub async fn list_payments(
) -> RouterResponse<api::PaymentListResponse> {
use futures::stream::StreamExt;
use crate::types::transformers::ForeignFrom;
use crate::types::transformers::ForeignTryFrom;
helpers::validate_payment_list_request(&constraints)?;
let merchant_id = &merchant.merchant_id;
@ -724,7 +683,11 @@ pub async fn list_payments(
.collect::<Vec<(storage::PaymentIntent, storage::PaymentAttempt)>>()
.await;
let data: Vec<api::PaymentsResponse> = pi.into_iter().map(ForeignFrom::foreign_from).collect();
let data: Vec<api::PaymentsResponse> = pi
.into_iter()
.map(ForeignTryFrom::foreign_try_from)
.collect::<Result<_, _>>()
.change_context(errors::ApiErrorResponse::InternalServerError)?;
Ok(services::ApplicationResponse::Json(
api::PaymentListResponse {
@ -766,47 +729,192 @@ pub async fn add_process_sync_task(
Ok(())
}
pub async fn route_connector<F>(
pub fn update_straight_through_routing<F>(
payment_data: &mut PaymentData<F>,
request_straight_through: serde_json::Value,
) -> CustomResult<(), errors::ParsingError>
where
F: Send + Clone,
{
let mut routing_data: storage::RoutingData = payment_data
.payment_attempt
.connector
.clone()
.unwrap_or_else(|| serde_json::json!({}))
.parse_value("RoutingData")
.attach_printable("Invalid routing data format in payment attempt")?;
let request_straight_through: api::RoutingAlgorithm = request_straight_through
.parse_value("RoutingAlgorithm")
.attach_printable("Invalid straight through routing rules format")?;
routing_data.algorithm = Some(request_straight_through);
let encoded_routing_data = Encode::<storage::RoutingData>::encode_to_value(&routing_data)
.attach_printable("Unable to serialize routing data to serde value")?;
payment_data.payment_attempt.connector = Some(encoded_routing_data);
Ok(())
}
pub async fn get_connector_choice<F, Req>(
operation: &BoxedOperation<'_, F, Req>,
state: &AppState,
req: &Req,
merchant_account: &storage::MerchantAccount,
payment_data: &mut PaymentData<F>,
) -> RouterResult<Option<api::ConnectorCallType>>
where
F: Send + Clone,
{
let connector_choice = operation
.to_domain()?
.get_connector(merchant_account, state, req)
.await?;
let connector = if should_call_connector(operation, payment_data) {
Some(match connector_choice {
api::ConnectorChoice::SessionMultiple(connectors) => {
api::ConnectorCallType::Multiple(connectors)
}
api::ConnectorChoice::StraightThrough(straight_through) => connector_selection(
state,
merchant_account,
payment_data,
Some(straight_through),
)?,
api::ConnectorChoice::Decide => {
connector_selection(state, merchant_account, payment_data, None)?
}
})
} else if let api::ConnectorChoice::StraightThrough(val) = connector_choice {
update_straight_through_routing(payment_data, val)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to update straight through routing algorithm")?;
None
} else {
None
};
Ok(connector)
}
pub fn connector_selection<F>(
state: &AppState,
merchant_account: &storage::MerchantAccount,
payment_data: &mut PaymentData<F>,
connector_call_type: api::ConnectorCallType,
request_straight_through: Option<serde_json::Value>,
) -> RouterResult<api::ConnectorCallType>
where
F: Send + Clone,
{
match connector_call_type {
api::ConnectorCallType::Single(connector) => {
payment_data.payment_attempt.connector = Some(connector.connector_name.to_string());
let mut routing_data: storage::RoutingData = payment_data
.payment_attempt
.connector
.clone()
.unwrap_or_else(|| serde_json::json!({}))
.parse_value("RoutingData")
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Invalid routing data format in payment attempt")?;
Ok(api::ConnectorCallType::Single(connector))
}
let request_straight_through: Option<api::RoutingAlgorithm> = request_straight_through
.map(|val| val.parse_value("RoutingAlgorithm"))
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Invalid straight through routing rules format")?;
api::ConnectorCallType::Routing => {
let routing_algorithm: api::RoutingAlgorithm = merchant_account
.routing_algorithm
.clone()
.parse_value("RoutingAlgorithm")
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Could not decode merchant routing rules")?;
let decided_connector = decide_connector(
state,
merchant_account,
request_straight_through,
&mut routing_data,
)?;
let connector_name = match routing_algorithm {
api::RoutingAlgorithm::Single(conn) => conn.to_string(),
};
let encoded_routing_data = Encode::<storage::RoutingData>::encode_to_value(&routing_data)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Unable to serialize routing data to serde value")?;
let connector_data = api::ConnectorData::get_connector_by_name(
&state.conf.connectors,
&connector_name,
api::GetToken::Connector,
)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Routing algorithm gave invalid connector")?;
payment_data.payment_attempt.connector = Some(encoded_routing_data);
payment_data.payment_attempt.connector = Some(connector_name);
Ok(api::ConnectorCallType::Single(connector_data))
}
call_type @ api::ConnectorCallType::Multiple(_) => Ok(call_type),
}
Ok(decided_connector)
}
pub fn decide_connector(
state: &AppState,
merchant_account: &storage::MerchantAccount,
request_straight_through: Option<api::RoutingAlgorithm>,
routing_data: &mut storage::RoutingData,
) -> RouterResult<api::ConnectorCallType> {
if let Some(ref connector_name) = routing_data.routed_through {
let connector_data = api::ConnectorData::get_connector_by_name(
&state.conf.connectors,
connector_name,
api::GetToken::Connector,
)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Invalid connector name received in 'routed_through'")?;
return Ok(api::ConnectorCallType::Single(connector_data));
}
if let Some(routing_algorithm) = request_straight_through {
let connector_name = match &routing_algorithm {
api::RoutingAlgorithm::Single(conn) => conn.to_string(),
};
let connector_data = api::ConnectorData::get_connector_by_name(
&state.conf.connectors,
&connector_name,
api::GetToken::Connector,
)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Invalid connector name received in routing algorithm")?;
routing_data.routed_through = Some(connector_name);
routing_data.algorithm = Some(routing_algorithm);
return Ok(api::ConnectorCallType::Single(connector_data));
}
if let Some(ref routing_algorithm) = routing_data.algorithm {
let connector_name = match routing_algorithm {
api::RoutingAlgorithm::Single(conn) => conn.to_string(),
};
let connector_data = api::ConnectorData::get_connector_by_name(
&state.conf.connectors,
&connector_name,
api::GetToken::Connector,
)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Invalid connector name received in routing algorithm")?;
routing_data.routed_through = Some(connector_name);
return Ok(api::ConnectorCallType::Single(connector_data));
}
let routing_algorithm: api::RoutingAlgorithm = merchant_account
.routing_algorithm
.clone()
.parse_value("RoutingAlgorithm")
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Unable to deserialize merchant routing algorithm")?;
let connector_name = match routing_algorithm {
api::RoutingAlgorithm::Single(conn) => conn.to_string(),
};
let connector_data = api::ConnectorData::get_connector_by_name(
&state.conf.connectors,
&connector_name,
api::GetToken::Connector,
)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Routing algorithm gave invalid connector")?;
routing_data.routed_through = Some(connector_name);
Ok(api::ConnectorCallType::Single(connector_data))
}

View File

@ -471,9 +471,14 @@ where
Op: std::fmt::Debug,
{
if check_if_operation_confirm(operation) {
let connector_name = payment_attempt
let routed_through: storage::RoutedThroughData = payment_attempt
.connector
.clone()
.parse_value("RoutedThroughData")
.change_context(errors::ApiErrorResponse::InternalServerError)?;
let connector_name = routed_through
.routed_through
.ok_or(errors::ApiErrorResponse::InternalServerError)?;
let schedule_time = payment_sync::get_sync_process_schedule_time(
@ -624,20 +629,13 @@ pub async fn get_customer_from_details<F: Clone>(
}
pub async fn get_connector_default(
state: &AppState,
request_connector: Option<&String>,
) -> CustomResult<api::ConnectorCallType, errors::ApiErrorResponse> {
let connectors = &state.conf.connectors;
if let Some(connector_name) = request_connector {
let connector_data = api::ConnectorData::get_connector_by_name(
connectors,
connector_name,
api::GetToken::Connector,
)?;
Ok(api::ConnectorCallType::Single(connector_data))
} else {
Ok(api::ConnectorCallType::Routing)
}
_state: &AppState,
request_connector: Option<serde_json::Value>,
) -> CustomResult<api::ConnectorChoice, errors::ApiErrorResponse> {
Ok(request_connector.map_or(
api::ConnectorChoice::Decide,
api::ConnectorChoice::StraightThrough,
))
}
#[instrument(skip_all)]

View File

@ -126,8 +126,7 @@ pub trait Domain<F: Clone, R>: Send + Sync {
merchant_account: &storage::MerchantAccount,
state: &AppState,
request: &R,
previously_used_connector: Option<&String>,
) -> CustomResult<api::ConnectorCallType, errors::ApiErrorResponse>;
) -> CustomResult<api::ConnectorChoice, errors::ApiErrorResponse>;
}
#[async_trait]
@ -195,9 +194,8 @@ where
_merchant_account: &storage::MerchantAccount,
state: &AppState,
_request: &api::PaymentsRetrieveRequest,
previously_used_connector: Option<&String>,
) -> CustomResult<api::ConnectorCallType, errors::ApiErrorResponse> {
helpers::get_connector_default(state, previously_used_connector).await
) -> CustomResult<api::ConnectorChoice, errors::ApiErrorResponse> {
helpers::get_connector_default(state, None).await
}
#[instrument(skip_all)]
@ -263,9 +261,8 @@ where
_merchant_account: &storage::MerchantAccount,
state: &AppState,
_request: &api::PaymentsCaptureRequest,
previously_used_connector: Option<&String>,
) -> CustomResult<api::ConnectorCallType, errors::ApiErrorResponse> {
helpers::get_connector_default(state, previously_used_connector).await
) -> CustomResult<api::ConnectorChoice, errors::ApiErrorResponse> {
helpers::get_connector_default(state, None).await
}
}
@ -319,8 +316,7 @@ where
_merchant_account: &storage::MerchantAccount,
state: &AppState,
_request: &api::PaymentsCancelRequest,
previously_used_connector: Option<&String>,
) -> CustomResult<api::ConnectorCallType, errors::ApiErrorResponse> {
helpers::get_connector_default(state, previously_used_connector).await
) -> CustomResult<api::ConnectorChoice, errors::ApiErrorResponse> {
helpers::get_connector_default(state, None).await
}
}

View File

@ -265,19 +265,10 @@ impl<F: Clone + Send> Domain<F, api::PaymentsRequest> for CompleteAuthorize {
_merchant_account: &storage::MerchantAccount,
state: &AppState,
request: &api::PaymentsRequest,
previously_used_connector: Option<&String>,
) -> CustomResult<api::ConnectorCallType, errors::ApiErrorResponse> {
) -> CustomResult<api::ConnectorChoice, errors::ApiErrorResponse> {
// Use a new connector in the confirm call or use the same one which was passed when
// creating the payment or if none is passed then use the routing algorithm
let request_connector = request
.connector
.as_ref()
.and_then(|connector| connector.first().map(|c| c.to_string()));
helpers::get_connector_default(
state,
request_connector.as_ref().or(previously_used_connector),
)
.await
helpers::get_connector_default(state, request.routing.clone()).await
}
}

View File

@ -287,19 +287,10 @@ impl<F: Clone + Send> Domain<F, api::PaymentsRequest> for PaymentConfirm {
_merchant_account: &storage::MerchantAccount,
state: &AppState,
request: &api::PaymentsRequest,
previously_used_connector: Option<&String>,
) -> CustomResult<api::ConnectorCallType, errors::ApiErrorResponse> {
) -> CustomResult<api::ConnectorChoice, errors::ApiErrorResponse> {
// Use a new connector in the confirm call or use the same one which was passed when
// creating the payment or if none is passed then use the routing algorithm
let request_connector = request
.connector
.as_ref()
.and_then(|connector| connector.first().map(|c| c.to_string()));
helpers::get_connector_default(
state,
request_connector.as_ref().or(previously_used_connector),
)
.await
helpers::get_connector_default(state, request.routing.clone()).await
}
}

View File

@ -23,6 +23,7 @@ use crate::{
storage::{
self,
enums::{self, IntentStatus},
PaymentAttemptExt,
},
transformers::ForeignInto,
},
@ -137,7 +138,8 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
})?;
connector_response = db
.insert_connector_response(
Self::make_connector_response(&payment_attempt),
Self::make_connector_response(&payment_attempt)
.change_context(errors::ApiErrorResponse::InternalServerError)?,
storage_scheme,
)
.await
@ -276,13 +278,8 @@ impl<F: Clone + Send> Domain<F, api::PaymentsRequest> for PaymentCreate {
_merchant_account: &storage::MerchantAccount,
state: &AppState,
request: &api::PaymentsRequest,
_previously_used_connector: Option<&String>,
) -> CustomResult<api::ConnectorCallType, errors::ApiErrorResponse> {
let request_connector = request
.connector
.as_ref()
.and_then(|connector| connector.first().map(|c| c.to_string()));
helpers::get_connector_default(state, request_connector.as_ref()).await
) -> CustomResult<api::ConnectorChoice, errors::ApiErrorResponse> {
helpers::get_connector_default(state, request.routing.clone()).await
}
}
@ -451,12 +448,6 @@ impl PaymentCreate {
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to encode additional pm data")?;
let connector = request.connector.as_ref().and_then(|connector_vec| {
connector_vec
.first()
.map(|first_connector| first_connector.to_string())
});
Ok(storage::PaymentAttemptNew {
payment_id: payment_id.to_string(),
merchant_id: merchant_id.to_string(),
@ -476,7 +467,6 @@ impl PaymentCreate {
payment_experience: request.payment_experience.map(ForeignInto::foreign_into),
payment_method_type: request.payment_method_type.map(ForeignInto::foreign_into),
payment_method_data: additional_pm_data,
connector,
..storage::PaymentAttemptNew::default()
})
}
@ -529,18 +519,18 @@ impl PaymentCreate {
#[instrument(skip_all)]
pub fn make_connector_response(
payment_attempt: &storage::PaymentAttempt,
) -> storage::ConnectorResponseNew {
storage::ConnectorResponseNew {
) -> CustomResult<storage::ConnectorResponseNew, errors::ParsingError> {
Ok(storage::ConnectorResponseNew {
payment_id: payment_attempt.payment_id.clone(),
merchant_id: payment_attempt.merchant_id.clone(),
attempt_id: payment_attempt.attempt_id.clone(),
created_at: payment_attempt.created_at,
modified_at: payment_attempt.modified_at,
connector_name: payment_attempt.connector.clone(),
connector_name: payment_attempt.get_routed_through_connector()?,
connector_transaction_id: None,
authentication_data: None,
encoded_data: None,
}
})
}
}

View File

@ -119,7 +119,8 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::VerifyRequest> for Paym
connector_response = match db
.insert_connector_response(
PaymentCreate::make_connector_response(&payment_attempt),
PaymentCreate::make_connector_response(&payment_attempt)
.change_context(errors::ApiErrorResponse::InternalServerError)?,
storage_scheme,
)
.await
@ -273,9 +274,8 @@ where
_merchant_account: &storage::MerchantAccount,
state: &AppState,
_request: &api::VerifyRequest,
previously_used_connector: Option<&String>,
) -> CustomResult<api::ConnectorCallType, errors::ApiErrorResponse> {
helpers::get_connector_default(state, previously_used_connector).await
) -> CustomResult<api::ConnectorChoice, errors::ApiErrorResponse> {
helpers::get_connector_default(state, None).await
}
}

View File

@ -293,7 +293,7 @@ async fn payment_response_update_tracker<F: Clone, T>(
let (payment_attempt_update, connector_response_update) = match router_data.response.clone() {
Err(err) => (
Some(storage::PaymentAttemptUpdate::ErrorUpdate {
connector: Some(router_data.connector.clone()),
connector: None,
status: storage::enums::AttemptStatus::Failure,
error_message: Some(err.message),
error_code: Some(err.code),
@ -316,7 +316,7 @@ async fn payment_response_update_tracker<F: Clone, T>(
};
let encoded_data = payment_data.connector_response.encoded_data.clone();
let connector_name = payment_data.payment_attempt.connector.clone();
let connector_name = router_data.connector.clone();
let authentication_data = redirection_data
.map(|data| utils::Encode::<RedirectForm>::encode_to_value(&data))
@ -330,7 +330,7 @@ async fn payment_response_update_tracker<F: Clone, T>(
let payment_attempt_update = storage::PaymentAttemptUpdate::ResponseUpdate {
status: router_data.status,
connector: Some(router_data.connector),
connector: None,
connector_transaction_id: connector_transaction_id.clone(),
authentication_type: None,
payment_method_id: Some(router_data.payment_method_id),
@ -345,7 +345,7 @@ async fn payment_response_update_tracker<F: Clone, T>(
connector_transaction_id,
authentication_data,
encoded_data,
connector_name,
connector_name: Some(connector_name),
};
(

View File

@ -293,8 +293,7 @@ where
merchant_account: &storage::MerchantAccount,
state: &AppState,
request: &api::PaymentsSessionRequest,
_previously_used_connector: Option<&String>,
) -> RouterResult<api::ConnectorCallType> {
) -> RouterResult<api::ConnectorChoice> {
let connectors = &state.conf.connectors;
let db = &state.store;
@ -434,6 +433,6 @@ where
connectors_data
};
Ok(api::ConnectorCallType::Multiple(connectors_data))
Ok(api::ConnectorChoice::SessionMultiple(connectors_data))
}
}

View File

@ -250,8 +250,7 @@ where
_merchant_account: &storage::MerchantAccount,
state: &AppState,
_request: &api::PaymentsStartRequest,
previously_used_connector: Option<&String>,
) -> CustomResult<api::ConnectorCallType, errors::ApiErrorResponse> {
helpers::get_connector_default(state, previously_used_connector).await
) -> CustomResult<api::ConnectorChoice, errors::ApiErrorResponse> {
helpers::get_connector_default(state, None).await
}
}

View File

@ -101,10 +101,9 @@ impl<F: Clone + Send> Domain<F, api::PaymentsRequest> for PaymentStatus {
&'a self,
_merchant_account: &storage::MerchantAccount,
state: &AppState,
_request: &api::PaymentsRequest,
previously_used_connector: Option<&String>,
) -> CustomResult<api::ConnectorCallType, errors::ApiErrorResponse> {
helpers::get_connector_default(state, previously_used_connector).await
request: &api::PaymentsRequest,
) -> CustomResult<api::ConnectorChoice, errors::ApiErrorResponse> {
helpers::get_connector_default(state, request.routing.clone()).await
}
}

View File

@ -316,10 +316,9 @@ impl<F: Clone + Send> Domain<F, api::PaymentsRequest> for PaymentUpdate {
&'a self,
_merchant_account: &storage::MerchantAccount,
state: &AppState,
_request: &api::PaymentsRequest,
previously_used_connector: Option<&String>,
) -> CustomResult<api::ConnectorCallType, errors::ApiErrorResponse> {
helpers::get_connector_default(state, previously_used_connector).await
request: &api::PaymentsRequest,
) -> CustomResult<api::ConnectorChoice, errors::ApiErrorResponse> {
helpers::get_connector_default(state, request.routing.clone()).await
}
}

View File

@ -14,8 +14,8 @@ use crate::{
services::{self, RedirectForm},
types::{
self, api,
storage::{self, enums},
transformers::{ForeignFrom, ForeignInto},
storage::{self, enums, PaymentAttemptExt},
transformers::{ForeignInto, ForeignTryFrom},
},
utils::{OptionExt, ValueExt},
};
@ -275,6 +275,9 @@ where
})
}
let mut response: api::PaymentsResponse = Default::default();
let routed_through = payment_attempt
.get_routed_through_connector()
.change_context(errors::ApiErrorResponse::InternalServerError)?;
services::ApplicationResponse::Json(
response
.set_payment_id(Some(payment_attempt.payment_id))
@ -283,7 +286,7 @@ where
.set_amount(payment_attempt.amount)
.set_amount_capturable(None)
.set_amount_received(payment_intent.amount_captured)
.set_connector(payment_attempt.connector)
.set_connector(routed_through)
.set_client_secret(payment_intent.client_secret.map(masking::Secret::new))
.set_created(Some(payment_intent.created_at))
.set_currency(currency)
@ -398,11 +401,15 @@ where
})
}
impl ForeignFrom<(storage::PaymentIntent, storage::PaymentAttempt)> for api::PaymentsResponse {
fn foreign_from(item: (storage::PaymentIntent, storage::PaymentAttempt)) -> Self {
impl ForeignTryFrom<(storage::PaymentIntent, storage::PaymentAttempt)> for api::PaymentsResponse {
type Error = error_stack::Report<errors::ParsingError>;
fn foreign_try_from(
item: (storage::PaymentIntent, storage::PaymentAttempt),
) -> Result<Self, Self::Error> {
let pi = item.0;
let pa = item.1;
Self {
Ok(Self {
payment_id: Some(pi.payment_id),
merchant_id: Some(pi.merchant_id),
status: pi.status.foreign_into(),
@ -414,11 +421,11 @@ impl ForeignFrom<(storage::PaymentIntent, storage::PaymentAttempt)> for api::Pay
description: pi.description,
metadata: pi.metadata,
customer_id: pi.customer_id,
connector: pa.connector,
connector: pa.get_routed_through_connector()?,
payment_method: pa.payment_method.map(ForeignInto::foreign_into),
payment_method_type: pa.payment_method_type.map(ForeignInto::foreign_into),
..Default::default()
}
})
}
}

View File

@ -18,7 +18,7 @@ use crate::{
types::{
self,
api::{self, refunds},
storage::{self, enums, ProcessTrackerExt},
storage::{self, enums, PaymentAttemptExt, ProcessTrackerExt},
transformers::{ForeignFrom, ForeignInto},
},
utils::{self, OptionExt},
@ -113,24 +113,25 @@ pub async fn trigger_refund_to_gateway(
payment_intent: &storage::PaymentIntent,
creds_identifier: Option<String>,
) -> RouterResult<storage::Refund> {
let connector = payment_attempt
.connector
.clone()
.ok_or(errors::ApiErrorResponse::InternalServerError)?;
let connector_id = connector.to_string();
let routed_through = payment_attempt
.get_routed_through_connector()
.change_context(errors::ApiErrorResponse::InternalServerError)?
.ok_or(errors::ApiErrorResponse::InternalServerError)
.into_report()
.attach_printable("Failed to retrieve connector from payment attempt")?;
metrics::REFUND_COUNT.add(
&metrics::CONTEXT,
1,
&[metrics::request::add_attributes(
"connector",
connector.to_string(),
routed_through.clone(),
)],
);
let connector: api::ConnectorData = api::ConnectorData::get_connector_by_name(
&state.conf.connectors,
&connector_id,
&routed_through,
api::GetToken::Connector,
)
.change_context(errors::ApiErrorResponse::InternalServerError)
@ -143,11 +144,11 @@ pub async fn trigger_refund_to_gateway(
.attach_printable("Transaction in invalid")
})?;
validator::validate_for_valid_refunds(payment_attempt)?;
validator::validate_for_valid_refunds(payment_attempt, connector.connector_name)?;
let mut router_data = core_utils::construct_refund_router_data(
state,
&connector_id,
&routed_through,
merchant_account,
(payment_attempt.amount, currency),
payment_intent,
@ -544,10 +545,12 @@ pub async fn validate_and_create_refund(
)
.change_context(errors::ApiErrorResponse::MaximumRefundCount)?;
let connector = payment_attempt.connector.clone().ok_or_else(|| {
report!(errors::ApiErrorResponse::InternalServerError)
.attach_printable("connector not populated in payment attempt.")
})?;
let connector = payment_attempt
.get_routed_through_connector()
.change_context(errors::ApiErrorResponse::InternalServerError)?
.ok_or(errors::ApiErrorResponse::InternalServerError)
.into_report()
.attach_printable("No connector populated in payment attempt")?;
refund_create_req = storage::RefundNew::default()
.set_refund_id(refund_id.to_string())

View File

@ -1,5 +1,4 @@
use common_utils::ext_traits::StringExt;
use error_stack::{report, IntoReport, ResultExt};
use error_stack::{report, IntoReport};
use router_env::{instrument, tracing};
use time::PrimitiveDateTime;
@ -141,14 +140,8 @@ pub fn validate_refund_list(limit: Option<i64>) -> CustomResult<i64, errors::Api
pub fn validate_for_valid_refunds(
payment_attempt: &storage_models::payment_attempt::PaymentAttempt,
connector: api_models::enums::Connector,
) -> RouterResult<()> {
let connector: api_models::enums::Connector = payment_attempt
.connector
.clone()
.get_required_value("connector")?
.parse_enum("connector")
.change_context(errors::ApiErrorResponse::IncorrectConnectorNameGiven)?;
let payment_method = payment_attempt
.payment_method
.as_ref()

View File

@ -9,7 +9,7 @@ use crate::{
scheduler::{consumer, process_data, utils},
types::{
api,
storage::{self, enums, ProcessTrackerExt},
storage::{self, enums, PaymentAttemptExt, ProcessTrackerExt},
},
utils::{OptionExt, ValueExt},
};
@ -64,9 +64,10 @@ impl ProcessTrackerWorkflow for PaymentsSyncWorkflow {
_ => {
let connector = payment_data
.payment_attempt
.connector
.clone()
.get_routed_through_connector()
.map_err(errors::ProcessTrackerError::EParsingError)?
.ok_or(errors::ProcessTrackerError::MissingRequiredField)?;
retry_sync_task(
db,
connector,

View File

@ -124,8 +124,13 @@ pub struct ConnectorData {
pub get_token: GetToken,
}
pub enum ConnectorChoice {
SessionMultiple(Vec<ConnectorData>),
StraightThrough(serde_json::Value),
Decide,
}
pub enum ConnectorCallType {
Routing,
Multiple(Vec<ConnectorData>),
Single(ConnectorData),
}

View File

@ -1,7 +1,43 @@
use error_stack::ResultExt;
pub use storage_models::payment_attempt::{
PaymentAttempt, PaymentAttemptNew, PaymentAttemptUpdate, PaymentAttemptUpdateInternal,
};
use crate::{
core::errors::{self, CustomResult},
utils::ValueExt,
};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct RoutingData {
pub routed_through: Option<String>,
pub algorithm: Option<api_models::admin::RoutingAlgorithm>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct RoutedThroughData {
pub routed_through: Option<String>,
}
pub trait PaymentAttemptExt {
fn get_routed_through_connector(&self) -> CustomResult<Option<String>, errors::ParsingError>;
}
impl PaymentAttemptExt for PaymentAttempt {
fn get_routed_through_connector(&self) -> CustomResult<Option<String>, errors::ParsingError> {
if let Some(ref val) = self.connector {
let data: RoutedThroughData = val
.clone()
.parse_value("RoutedThroughData")
.attach_printable("Failed to read routed_through connector from payment attempt")?;
Ok(data.routed_through)
} else {
Ok(None)
}
}
}
#[cfg(feature = "kv_store")]
impl crate::utils::storage_partitioning::KvStorePartition for PaymentAttempt {}
@ -28,9 +64,12 @@ mod tests {
let payment_id = Uuid::new_v4().to_string();
let current_time = common_utils::date_time::now();
let connector = types::Connector::Dummy.to_string();
let payment_attempt = PaymentAttemptNew {
payment_id: payment_id.clone(),
connector: Some(types::Connector::Dummy.to_string()),
connector: Some(serde_json::json!({
"routed_through": connector,
})),
created_at: current_time.into(),
modified_at: current_time.into(),
..PaymentAttemptNew::default()
@ -57,11 +96,14 @@ mod tests {
let current_time = common_utils::date_time::now();
let payment_id = Uuid::new_v4().to_string();
let merchant_id = Uuid::new_v4().to_string();
let connector = types::Connector::Dummy.to_string();
let payment_attempt = PaymentAttemptNew {
payment_id: payment_id.clone(),
merchant_id: merchant_id.clone(),
connector: Some(types::Connector::Dummy.to_string()),
connector: Some(serde_json::json!({
"routed_through": connector,
})),
created_at: current_time.into(),
modified_at: current_time.into(),
..PaymentAttemptNew::default()
@ -96,11 +138,14 @@ mod tests {
let uuid = uuid::Uuid::new_v4().to_string();
let state = routes::AppState::with_storage(conf, StorageImpl::PostgresqlTest).await;
let current_time = common_utils::date_time::now();
let connector = types::Connector::Dummy.to_string();
let payment_attempt = PaymentAttemptNew {
payment_id: uuid.clone(),
merchant_id: "1".to_string(),
connector: Some(types::Connector::Dummy.to_string()),
connector: Some(serde_json::json!({
"routed_through": connector,
})),
created_at: current_time.into(),
modified_at: current_time.into(),
// Adding a mandate_id

View File

@ -288,7 +288,6 @@ async fn payments_create_core() {
)),
merchant_id: Some("jarnura".to_string()),
amount: Some(6540.into()),
connector: None,
currency: Some(api_enums::Currency::USD),
capture_method: Some(api_enums::CaptureMethod::Automatic),
amount_to_capture: Some(6540),
@ -438,7 +437,6 @@ async fn payments_create_core_adyen_no_redirect() {
payment_id: Some(api::PaymentIdType::PaymentIntentId(payment_id.clone())),
merchant_id: Some(merchant_id.clone()),
amount: Some(6540.into()),
connector: None,
currency: Some(api_enums::Currency::USD),
capture_method: Some(api_enums::CaptureMethod::Automatic),
amount_to_capture: Some(6540),

View File

@ -203,7 +203,6 @@ async fn payments_create_core_adyen_no_redirect() {
payment_id: Some(api::PaymentIdType::PaymentIntentId(payment_id.clone())),
merchant_id: Some(merchant_id.clone()),
amount: Some(6540.into()),
connector: None,
currency: Some(api_enums::Currency::USD),
capture_method: Some(api_enums::CaptureMethod::Automatic),
amount_to_capture: Some(6540),

View File

@ -15,7 +15,7 @@ pub struct PaymentAttempt {
pub amount: i64,
pub currency: Option<storage_enums::Currency>,
pub save_to_locker: Option<bool>,
pub connector: Option<String>,
pub connector: Option<serde_json::Value>,
pub error_message: Option<String>,
pub offer_amount: Option<i64>,
pub surcharge_amount: Option<i64>,
@ -59,7 +59,7 @@ pub struct PaymentAttemptNew {
pub currency: Option<storage_enums::Currency>,
// pub auto_capture: Option<bool>,
pub save_to_locker: Option<bool>,
pub connector: Option<String>,
pub connector: Option<serde_json::Value>,
pub error_message: Option<String>,
pub offer_amount: Option<i64>,
pub surcharge_amount: Option<i64>,
@ -105,7 +105,7 @@ pub enum PaymentAttemptUpdate {
},
UpdateTrackers {
payment_token: Option<String>,
connector: Option<String>,
connector: Option<serde_json::Value>,
},
AuthenticationTypeUpdate {
authentication_type: storage_enums::AuthenticationType,
@ -117,7 +117,7 @@ pub enum PaymentAttemptUpdate {
authentication_type: Option<storage_enums::AuthenticationType>,
payment_method: Option<storage_enums::PaymentMethod>,
browser_info: Option<serde_json::Value>,
connector: Option<String>,
connector: Option<serde_json::Value>,
payment_token: Option<String>,
payment_method_data: Option<serde_json::Value>,
payment_method_type: Option<storage_enums::PaymentMethodType>,
@ -129,7 +129,7 @@ pub enum PaymentAttemptUpdate {
},
ResponseUpdate {
status: storage_enums::AttemptStatus,
connector: Option<String>,
connector: Option<serde_json::Value>,
connector_transaction_id: Option<String>,
authentication_type: Option<storage_enums::AuthenticationType>,
payment_method_id: Option<Option<String>>,
@ -140,7 +140,7 @@ pub enum PaymentAttemptUpdate {
status: storage_enums::AttemptStatus,
},
ErrorUpdate {
connector: Option<String>,
connector: Option<serde_json::Value>,
status: storage_enums::AttemptStatus,
error_code: Option<String>,
error_message: Option<String>,
@ -154,7 +154,7 @@ pub struct PaymentAttemptUpdateInternal {
currency: Option<storage_enums::Currency>,
status: Option<storage_enums::AttemptStatus>,
connector_transaction_id: Option<String>,
connector: Option<String>,
connector: Option<serde_json::Value>,
authentication_type: Option<storage_enums::AuthenticationType>,
payment_method: Option<storage_enums::PaymentMethod>,
error_message: Option<String>,

View File

@ -211,7 +211,7 @@ diesel::table! {
amount -> Int8,
currency -> Nullable<Currency>,
save_to_locker -> Nullable<Bool>,
connector -> Nullable<Varchar>,
connector -> Nullable<Jsonb>,
error_message -> Nullable<Text>,
offer_amount -> Nullable<Int8>,
surcharge_amount -> Nullable<Int8>,

View File

@ -0,0 +1,5 @@
-- Alter column type to varchar(64) and extract and set the connector
-- name field from the json.
ALTER TABLE payment_attempt
ALTER COLUMN connector TYPE VARCHAR(64)
USING connector->>'routed_through';

View File

@ -0,0 +1,8 @@
-- Alter column type to json
-- as well as the connector.
ALTER TABLE payment_attempt
ALTER COLUMN connector TYPE JSONB
USING jsonb_build_object(
'routed_through', connector,
'algorithm', NULL
);